parent
4299588806
commit
bf40bc4fe3
@ -1,5 +1,6 @@
|
|||||||
# DIFFRENCE
|
# DIFFRENCE
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
## 2024.9.0-yami-1.3.1
|
## 2024.9.0-yami-1.3.1
|
||||||
## Client
|
## Client
|
||||||
- フォロー/フォロワー/アナウンス/みつける/Play/ギャラリー/チャンネル/TL/ユーザー/ノートのページをログイン必須に
|
- フォロー/フォロワー/アナウンス/みつける/Play/ギャラリー/チャンネル/TL/ユーザー/ノートのページをログイン必須に
|
||||||
@ -9,12 +10,17 @@
|
|||||||
## Feat
|
## Feat
|
||||||
- ロールで引用通知の設定を制限出来るように
|
- ロールで引用通知の設定を制限出来るように
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 3c53bbbac6 (Merge pull request #40 from lqvp/master)
|
||||||
## 2024.9.0-yami-1.2.9
|
## 2024.9.0-yami-1.2.9
|
||||||
## Feat
|
## Feat
|
||||||
- ノート数を隠せるように(連合しません)
|
- ノート数を隠せるように(連合しません)
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
=======
|
=======
|
||||||
>>>>>>> 00cf91cf30 (Merge pull request #39 from lqvp/master)
|
>>>>>>> 00cf91cf30 (Merge pull request #39 from lqvp/master)
|
||||||
|
=======
|
||||||
|
>>>>>>> 3c53bbbac6 (Merge pull request #40 from lqvp/master)
|
||||||
## 2024.9.0-yami-1.2.8
|
## 2024.9.0-yami-1.2.8
|
||||||
## Feat
|
## Feat
|
||||||
- Cherry-Pick アクティビティの非公開機能(hideki0403/kakurega.app)
|
- Cherry-Pick アクティビティの非公開機能(hideki0403/kakurega.app)
|
||||||
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@ -3788,6 +3788,10 @@ export interface Locale extends ILocale {
|
|||||||
* スレッドのミュートを解除
|
* スレッドのミュートを解除
|
||||||
*/
|
*/
|
||||||
"unmuteThread": string;
|
"unmuteThread": string;
|
||||||
|
/**
|
||||||
|
* ノート数の公開範囲
|
||||||
|
*/
|
||||||
|
"notesVisibility": string;
|
||||||
/**
|
/**
|
||||||
* フォローの公開範囲
|
* フォローの公開範囲
|
||||||
*/
|
*/
|
||||||
|
@ -943,6 +943,7 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を
|
|||||||
classic: "クラシック"
|
classic: "クラシック"
|
||||||
muteThread: "スレッドをミュート"
|
muteThread: "スレッドをミュート"
|
||||||
unmuteThread: "スレッドのミュートを解除"
|
unmuteThread: "スレッドのミュートを解除"
|
||||||
|
notesVisibility: "ノート数の公開範囲"
|
||||||
followingVisibility: "フォローの公開範囲"
|
followingVisibility: "フォローの公開範囲"
|
||||||
followersVisibility: "フォロワーの公開範囲"
|
followersVisibility: "フォロワーの公開範囲"
|
||||||
continueThread: "さらにスレッドを見る"
|
continueThread: "さらにスレッドを見る"
|
||||||
|
22
packages/backend/migration/1702718871542-notesvisibility.js
Normal file
22
packages/backend/migration/1702718871542-notesvisibility.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class notesvisibility1702718871542 {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'notesvisibility1702718871542';
|
||||||
|
}
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_notesVisibility_enum" AS ENUM('public', 'followers', 'private')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "notesVisibility" "public"."user_profile_notesVisibility_enum" NOT NULL DEFAULT 'public'`);
|
||||||
|
await queryRunner.query(`UPDATE "user_profile" SET "notesVisibility" = 'public'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "notesVisibility"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."user_profile_notesVisibility_enum"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
436
packages/backend/src/core/WebhookTestService.ts
Normal file
436
packages/backend/src/core/WebhookTestService.ts
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
|
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
|
||||||
|
return {
|
||||||
|
id: 'dummy-abuse-report1',
|
||||||
|
targetUserId: 'dummy-target-user',
|
||||||
|
targetUser: null,
|
||||||
|
reporterId: 'dummy-reporter-user',
|
||||||
|
reporter: null,
|
||||||
|
assigneeId: null,
|
||||||
|
assignee: null,
|
||||||
|
resolved: false,
|
||||||
|
forwarded: false,
|
||||||
|
comment: 'This is a dummy report for testing purposes.',
|
||||||
|
targetUserHost: null,
|
||||||
|
reporterHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
|
return {
|
||||||
|
id: 'dummy-user-1',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 7),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
|
||||||
|
hideOnlineStatus: false,
|
||||||
|
username: 'dummy1',
|
||||||
|
usernameLower: 'dummy1',
|
||||||
|
name: 'DummyUser1',
|
||||||
|
followersCount: 10,
|
||||||
|
followingCount: 5,
|
||||||
|
movedToUri: null,
|
||||||
|
movedAt: null,
|
||||||
|
alsoKnownAs: null,
|
||||||
|
notesCount: 30,
|
||||||
|
avatarId: null,
|
||||||
|
avatar: null,
|
||||||
|
bannerId: null,
|
||||||
|
banner: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
bannerUrl: null,
|
||||||
|
avatarBlurhash: null,
|
||||||
|
bannerBlurhash: null,
|
||||||
|
avatarDecorations: [],
|
||||||
|
tags: [],
|
||||||
|
isSuspended: false,
|
||||||
|
isLocked: false,
|
||||||
|
isBot: false,
|
||||||
|
isCat: true,
|
||||||
|
isRoot: false,
|
||||||
|
isExplorable: true,
|
||||||
|
isHibernated: false,
|
||||||
|
isDeleted: false,
|
||||||
|
emojis: [],
|
||||||
|
score: 0,
|
||||||
|
host: null,
|
||||||
|
inbox: null,
|
||||||
|
sharedInbox: null,
|
||||||
|
featured: null,
|
||||||
|
uri: null,
|
||||||
|
followersUri: null,
|
||||||
|
token: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||||
|
return {
|
||||||
|
id: 'dummy-note-1',
|
||||||
|
replyId: null,
|
||||||
|
reply: null,
|
||||||
|
renoteId: null,
|
||||||
|
renote: null,
|
||||||
|
threadId: null,
|
||||||
|
text: 'This is a dummy note for testing purposes.',
|
||||||
|
name: null,
|
||||||
|
cw: null,
|
||||||
|
userId: 'dummy-user-1',
|
||||||
|
user: null,
|
||||||
|
localOnly: true,
|
||||||
|
reactionAcceptance: 'likeOnly',
|
||||||
|
renoteCount: 10,
|
||||||
|
repliesCount: 5,
|
||||||
|
clippedCount: 0,
|
||||||
|
reactions: {},
|
||||||
|
visibility: 'public',
|
||||||
|
uri: null,
|
||||||
|
url: null,
|
||||||
|
fileIds: [],
|
||||||
|
attachedFileTypes: [],
|
||||||
|
visibleUserIds: [],
|
||||||
|
mentions: [],
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
reactionAndUserPairCache: [],
|
||||||
|
emojis: [],
|
||||||
|
tags: [],
|
||||||
|
hasPoll: false,
|
||||||
|
channelId: null,
|
||||||
|
channel: null,
|
||||||
|
userHost: null,
|
||||||
|
replyUserId: null,
|
||||||
|
replyUserHost: null,
|
||||||
|
renoteUserId: null,
|
||||||
|
renoteUserHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
deletedAt: null,
|
||||||
|
text: note.text,
|
||||||
|
cw: note.cw,
|
||||||
|
userId: note.userId,
|
||||||
|
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||||
|
replyId: note.replyId,
|
||||||
|
renoteId: note.renoteId,
|
||||||
|
isHidden: false,
|
||||||
|
visibility: note.visibility,
|
||||||
|
mentions: note.mentions,
|
||||||
|
visibleUserIds: note.visibleUserIds,
|
||||||
|
fileIds: note.fileIds,
|
||||||
|
files: [],
|
||||||
|
tags: note.tags,
|
||||||
|
poll: null,
|
||||||
|
emojis: note.emojis,
|
||||||
|
channelId: note.channelId,
|
||||||
|
channel: note.channel,
|
||||||
|
localOnly: note.localOnly,
|
||||||
|
reactionAcceptance: note.reactionAcceptance,
|
||||||
|
reactionEmojis: {},
|
||||||
|
reactions: {},
|
||||||
|
reactionCount: 0,
|
||||||
|
renoteCount: note.renoteCount,
|
||||||
|
repliesCount: note.repliesCount,
|
||||||
|
uri: note.uri ?? undefined,
|
||||||
|
url: note.url ?? undefined,
|
||||||
|
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||||
|
...(detail ? {
|
||||||
|
clippedCount: note.clippedCount,
|
||||||
|
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||||
|
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||||
|
myReaction: null,
|
||||||
|
} : {}),
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
host: user.host,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
|
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||||
|
id: it.id,
|
||||||
|
angle: it.angle,
|
||||||
|
flipH: it.flipH,
|
||||||
|
url: 'https://example.com/dummy-image001.png',
|
||||||
|
offsetX: it.offsetX,
|
||||||
|
offsetY: it.offsetY,
|
||||||
|
})),
|
||||||
|
isBot: user.isBot,
|
||||||
|
isCat: user.isCat,
|
||||||
|
emojis: user.emojis,
|
||||||
|
onlineStatus: 'active',
|
||||||
|
badgeRoles: [],
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||||
|
return {
|
||||||
|
...toPackedUserLite(user),
|
||||||
|
url: null,
|
||||||
|
uri: null,
|
||||||
|
movedTo: null,
|
||||||
|
alsoKnownAs: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||||
|
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||||
|
bannerUrl: user.bannerUrl,
|
||||||
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
isLocked: user.isLocked,
|
||||||
|
isSilenced: false,
|
||||||
|
isSuspended: user.isSuspended,
|
||||||
|
description: null,
|
||||||
|
location: null,
|
||||||
|
birthday: null,
|
||||||
|
lang: null,
|
||||||
|
fields: [],
|
||||||
|
verifiedLinks: [],
|
||||||
|
followersCount: user.followersCount,
|
||||||
|
followingCount: user.followingCount,
|
||||||
|
notesCount: user.notesCount,
|
||||||
|
pinnedNoteIds: [],
|
||||||
|
pinnedNotes: [],
|
||||||
|
pinnedPageId: null,
|
||||||
|
pinnedPage: null,
|
||||||
|
publicReactions: true,
|
||||||
|
notesVisibility: 'public',
|
||||||
|
followersVisibility: 'public',
|
||||||
|
followingVisibility: 'public',
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
usePasswordLessLogin: false,
|
||||||
|
securityKeys: false,
|
||||||
|
roles: [],
|
||||||
|
memo: null,
|
||||||
|
moderationNote: undefined,
|
||||||
|
isFollowing: false,
|
||||||
|
isFollowed: false,
|
||||||
|
hasPendingFollowRequestFromYou: false,
|
||||||
|
hasPendingFollowRequestToYou: false,
|
||||||
|
isBlocking: false,
|
||||||
|
isBlocked: false,
|
||||||
|
isMuted: false,
|
||||||
|
isRenoteMuted: false,
|
||||||
|
notify: 'none',
|
||||||
|
withReplies: true,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyUser1 = generateDummyUser();
|
||||||
|
const dummyUser2 = generateDummyUser({
|
||||||
|
id: 'dummy-user-2',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 30),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis),
|
||||||
|
username: 'dummy2',
|
||||||
|
usernameLower: 'dummy2',
|
||||||
|
name: 'DummyUser2',
|
||||||
|
followersCount: 40,
|
||||||
|
followingCount: 50,
|
||||||
|
notesCount: 900,
|
||||||
|
});
|
||||||
|
const dummyUser3 = generateDummyUser({
|
||||||
|
id: 'dummy-user-3',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 15),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
username: 'dummy3',
|
||||||
|
usernameLower: 'dummy3',
|
||||||
|
name: 'DummyUser3',
|
||||||
|
followersCount: 60,
|
||||||
|
followingCount: 70,
|
||||||
|
notesCount: 15900,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebhookTestService {
|
||||||
|
public static NoSuchWebhookError = class extends Error {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userWebhookService: UserWebhookService,
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(active)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testUserWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiWebhook['id'],
|
||||||
|
type: WebhookEventTypes,
|
||||||
|
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
sender: MiUser | null,
|
||||||
|
) {
|
||||||
|
const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
|
||||||
|
.then(it => it.filter(it => it.userId === sender?.id));
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummyNote1 = generateDummyNote({
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyReply1 = generateDummyNote({
|
||||||
|
id: 'dummy-reply-1',
|
||||||
|
replyId: dummyNote1.id,
|
||||||
|
reply: dummyNote1,
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyRenote1 = generateDummyNote({
|
||||||
|
id: 'dummy-renote-1',
|
||||||
|
renoteId: dummyNote1.id,
|
||||||
|
renote: dummyNote1,
|
||||||
|
userId: dummyUser2.id,
|
||||||
|
user: dummyUser2,
|
||||||
|
text: null,
|
||||||
|
});
|
||||||
|
const dummyMention1 = generateDummyNote({
|
||||||
|
id: 'dummy-mention-1',
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
text: `@${dummyUser2.username} This is a mention to you.`,
|
||||||
|
mentions: [dummyUser2.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'note': {
|
||||||
|
send(toPackedNote(dummyNote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reply': {
|
||||||
|
send(toPackedNote(dummyReply1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'renote': {
|
||||||
|
send(toPackedNote(dummyRenote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mention': {
|
||||||
|
send(toPackedNote(dummyMention1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'follow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'followed': {
|
||||||
|
send(toPackedUserLite(dummyUser2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unfollow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(isActive)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testSystemWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiSystemWebhook['id'],
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'abuseReport': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'abuseReportResolved': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
assigneeId: dummyUser3.id,
|
||||||
|
assignee: dummyUser3,
|
||||||
|
resolved: true,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userCreated': {
|
||||||
|
send(toPackedUserLite(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -49,6 +49,7 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
|||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||||
|
import unfavorite from '@/server/api/endpoints/channels/unfavorite.js';
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
@ -308,15 +309,16 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
|
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
|
||||||
|
|
||||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
const [notesVisibility, followingVisibility, followersVisibility] = await Promise.all(
|
||||||
[
|
[
|
||||||
|
this.isPublicCollection(person.notes, resolver),
|
||||||
this.isPublicCollection(person.following, resolver),
|
this.isPublicCollection(person.following, resolver),
|
||||||
this.isPublicCollection(person.followers, resolver),
|
this.isPublicCollection(person.followers, resolver),
|
||||||
].map((p): Promise<'public' | 'private'> => p
|
].map((p): Promise<'public' | 'private'> => p
|
||||||
.then(isPublic => isPublic ? 'public' : 'private')
|
.then(isPublic => isPublic ? 'public' : 'private')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
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 notes/following/followers collection', { stack: err });
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
})
|
||||||
@ -397,6 +399,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
description: _description,
|
description: _description,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
|
notesVisibility,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
followersVisibility,
|
followersVisibility,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
@ -507,15 +510,16 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||||
|
|
||||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
const [notesVisibility, followingVisibility, followersVisibility] = await Promise.all(
|
||||||
[
|
[
|
||||||
|
this.isPublicCollection(person.notes, resolver),
|
||||||
this.isPublicCollection(person.following, resolver),
|
this.isPublicCollection(person.following, resolver),
|
||||||
this.isPublicCollection(person.followers, resolver),
|
this.isPublicCollection(person.followers, resolver),
|
||||||
].map((p): Promise<'public' | 'private' | undefined> => p
|
].map((p): Promise<'public' | 'private' | undefined> => p
|
||||||
.then(isPublic => isPublic ? 'public' : 'private')
|
.then(isPublic => isPublic ? 'public' : 'private')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
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 notes/following/followers collection', { stack: err });
|
||||||
// Do not update the visibiility on transient errors.
|
// Do not update the visibiility on transient errors.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -595,6 +599,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
notesVisibility,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
followersVisibility,
|
followersVisibility,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
|
@ -185,6 +185,7 @@ export interface IActor extends IObject {
|
|||||||
id: string;
|
id: string;
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
};
|
};
|
||||||
|
notes?: string | ICollection | IOrderedCollection;
|
||||||
followers?: string | ICollection | IOrderedCollection;
|
followers?: string | ICollection | IOrderedCollection;
|
||||||
following?: string | ICollection | IOrderedCollection;
|
following?: string | ICollection | IOrderedCollection;
|
||||||
featured?: string | IOrderedCollection;
|
featured?: string | IOrderedCollection;
|
||||||
|
@ -489,6 +489,10 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
|
const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
|
||||||
|
const notesCount = profile == null ? null :
|
||||||
|
(profile.notesVisibility === 'public') || isMe || iAmModerator ? user.notesCount :
|
||||||
|
(profile.notesVisibility === 'followers') && (relation && relation.isFollowing) ? user.notesCount :
|
||||||
|
null;
|
||||||
|
|
||||||
const followingCount = profile == null ? null :
|
const followingCount = profile == null ? null :
|
||||||
(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
|
(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
|
||||||
@ -544,7 +548,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
} : undefined) : undefined,
|
} : undefined) : undefined,
|
||||||
followersCount: followersCount ?? 0,
|
followersCount: followersCount ?? 0,
|
||||||
followingCount: followingCount ?? 0,
|
followingCount: followingCount ?? 0,
|
||||||
notesCount: user.notesCount,
|
notesCount: notesCount ?? 0,
|
||||||
emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost),
|
emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
// パフォーマンス上の理由でローカルユーザーのみ
|
// パフォーマンス上の理由でローカルユーザーのみ
|
||||||
@ -588,6 +592,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
|
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: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||||
hideActivity: this.isLocalUser(user) ? profile!.hideActivity : false, //
|
hideActivity: this.isLocalUser(user) ? profile!.hideActivity : false, //
|
||||||
|
notesVisibility: profile!.notesVisibility,
|
||||||
followersVisibility: profile!.followersVisibility,
|
followersVisibility: profile!.followersVisibility,
|
||||||
followingVisibility: profile!.followingVisibility,
|
followingVisibility: profile!.followingVisibility,
|
||||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||||
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
|
import { obsoleteNotificationTypes, notesVisibilities, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiPage } from './Page.js';
|
import { MiPage } from './Page.js';
|
||||||
@ -100,6 +100,12 @@ export class MiUserProfile {
|
|||||||
})
|
})
|
||||||
public publicReactions: boolean;
|
public publicReactions: boolean;
|
||||||
|
|
||||||
|
@Column('enum', {
|
||||||
|
enum: notesVisibilities,
|
||||||
|
default: 'public',
|
||||||
|
})
|
||||||
|
public notesVisibility: typeof notesVisibilities[number]
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('enum', {
|
||||||
enum: followingVisibilities,
|
enum: followingVisibilities,
|
||||||
default: 'public',
|
default: 'public',
|
||||||
|
@ -373,6 +373,11 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
notesVisibility: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
enum: ['public', 'followers', 'private'],
|
||||||
|
},
|
||||||
followingVisibility: {
|
followingVisibility: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -195,6 +195,7 @@ export const paramDef = {
|
|||||||
receiveAnnouncementEmail: { type: 'boolean' },
|
receiveAnnouncementEmail: { type: 'boolean' },
|
||||||
alwaysMarkNsfw: { type: 'boolean' },
|
alwaysMarkNsfw: { type: 'boolean' },
|
||||||
autoSensitive: { type: 'boolean' },
|
autoSensitive: { type: 'boolean' },
|
||||||
|
notesVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
@ -289,6 +290,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz;
|
if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz;
|
||||||
|
if (ps.notesVisibility !== undefined) profileUpdates.notesVisibility = ps.notesVisibility;
|
||||||
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
||||||
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { MiMeta, UsersRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@ -16,6 +16,9 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||||
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -40,6 +43,12 @@ export const meta = {
|
|||||||
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
forbidden: {
|
||||||
|
message: 'Forbidden.',
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
||||||
|
},
|
||||||
|
|
||||||
bothWithRepliesAndWithFiles: {
|
bothWithRepliesAndWithFiles: {
|
||||||
message: 'Specifying both withReplies and withFiles is not supported',
|
message: 'Specifying both withReplies and withFiles is not supported',
|
||||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||||
@ -62,22 +71,45 @@ export const paramDef = {
|
|||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
|
|
||||||
|
username: { type: 'string' },
|
||||||
|
host: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
anyOf: [
|
||||||
|
{ required: ['userId'] },
|
||||||
|
{ required: ['username', 'host'] },
|
||||||
|
],
|
||||||
required: ['userId'],
|
required: ['userId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private serverSettings: MiMeta,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
private utilityService: UtilityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
@ -88,6 +120,38 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy(ps.userId != null
|
||||||
|
? { id: ps.userId }
|
||||||
|
: { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
|
if (profile.notesVisibility !== 'public' && !await this.roleService.isModerator(me)) {
|
||||||
|
if (profile.notesVisibility === 'private') {
|
||||||
|
if (me == null || (me.id !== user.id)) {
|
||||||
|
throw new ApiError(meta.errors.forbidden);
|
||||||
|
}
|
||||||
|
} else if (profile.notesVisibility === 'followers') {
|
||||||
|
if (me == null) {
|
||||||
|
throw new ApiError(meta.errors.forbidden);
|
||||||
|
} else if (me.id !== user.id) {
|
||||||
|
const isFollowing = await this.notesRepository.exists({
|
||||||
|
where: {
|
||||||
|
followeeId: user.id,
|
||||||
|
followerId: me.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!isFollowing) {
|
||||||
|
throw new ApiError(meta.errors.forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// early return if me is blocked by requesting user
|
// early return if me is blocked by requesting user
|
||||||
if (me != null) {
|
if (me != null) {
|
||||||
const userIdsWhoBlockingMe = await this.cacheService.userBlockedCache.fetch(me.id);
|
const userIdsWhoBlockingMe = await this.cacheService.userBlockedCache.fetch(me.id);
|
||||||
|
@ -62,8 +62,8 @@ export class FeedService {
|
|||||||
id: author.link,
|
id: author.link,
|
||||||
title: `${author.name} (@${user.username}@${this.config.host})`,
|
title: `${author.name} (@${user.username}@${this.config.host})`,
|
||||||
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
|
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
|
||||||
generator: 'Sharkey',
|
generator: 'Misskey',
|
||||||
description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
description: `${(profile.notesVisibility === 'public') ? user.notesCount : '?'} Notes ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
||||||
link: author.link,
|
link: author.link,
|
||||||
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||||
feedLinks: {
|
feedLinks: {
|
||||||
|
@ -49,6 +49,7 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
|
|||||||
|
|
||||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||||
|
|
||||||
|
export const notesVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
export const followingVisibilities = ['public', 'followers', 'private'] as const;
|
export const followingVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
export const followersVisibilities = ['public', 'followers', 'private'] as const;
|
export const followersVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.status">
|
<div :class="$style.status">
|
||||||
<div :class="$style.statusItem">
|
<div v-if="isNotesVisibilityForMe(user)" :class="$style.statusItem">
|
||||||
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
|
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
|
<div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
|
||||||
@ -40,7 +40,7 @@ import number from '@/filters/number.js';
|
|||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
import { isNotesVisibilityForMe, isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.status">
|
<div :class="$style.status">
|
||||||
<div :class="$style.statusItem">
|
<div v-if="isNotesVisibilityForMe(user)" :class="$style.statusItem">
|
||||||
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
|
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
|
||||||
<div>{{ number(user.notesCount) }}</div>
|
<div>{{ number(user.notesCount) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -78,7 +78,7 @@ import number from '@/filters/number.js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
import { isNotesVisibilityForMe, isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -22,6 +22,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #caption>{{ i18n.ts.hideActivityDescription }}</template>
|
<template #caption>{{ i18n.ts.hideActivityDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSelect v-model="notesVisibility" @update:modelValue="save()">
|
||||||
|
<template #label>{{ i18n.ts.notesVisibility }}</template>
|
||||||
|
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
|
||||||
|
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
|
||||||
|
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
|
||||||
<MkSelect v-model="followingVisibility" @update:modelValue="save()">
|
<MkSelect v-model="followingVisibility" @update:modelValue="save()">
|
||||||
<template #label>{{ i18n.ts.followingVisibility }}</template>
|
<template #label>{{ i18n.ts.followingVisibility }}</template>
|
||||||
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
|
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
|
||||||
@ -103,6 +110,7 @@ const isExplorable = ref($i.isExplorable);
|
|||||||
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
||||||
const publicReactions = ref($i.publicReactions);
|
const publicReactions = ref($i.publicReactions);
|
||||||
const hideActivity = ref($i.hideActivity);
|
const hideActivity = ref($i.hideActivity);
|
||||||
|
const notesVisibility = ref($i.notesVisibility);
|
||||||
const followingVisibility = ref($i.followingVisibility);
|
const followingVisibility = ref($i.followingVisibility);
|
||||||
const followersVisibility = ref($i.followersVisibility);
|
const followersVisibility = ref($i.followersVisibility);
|
||||||
|
|
||||||
@ -122,6 +130,7 @@ function save() {
|
|||||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||||
publicReactions: !!publicReactions.value,
|
publicReactions: !!publicReactions.value,
|
||||||
hideActivity: !!hideActivity.value,
|
hideActivity: !!hideActivity.value,
|
||||||
|
notesVisibility: notesVisibility.value,
|
||||||
followingVisibility: followingVisibility.value,
|
followingVisibility: followingVisibility.value,
|
||||||
followersVisibility: followersVisibility.value,
|
followersVisibility: followersVisibility.value,
|
||||||
});
|
});
|
||||||
|
@ -103,7 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<MkA :to="userPage(user)">
|
<MkA v-if="isNotesVisibilityForMe(user)" :to="userPage(user, 'notes')">
|
||||||
<b>{{ number(user.notesCount) }}</b>
|
<b>{{ number(user.notesCount) }}</b>
|
||||||
<span>{{ i18n.ts.notes }}</span>
|
<span>{{ i18n.ts.notes }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
@ -186,7 +186,7 @@ import { $i, iAmModerator } from '@/account.js';
|
|||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
import { isNotesVisibilityForMe, isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
import { useRouter } from '@/router/supplier.js';
|
import { useRouter } from '@/router/supplier.js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
|
@ -6,6 +6,14 @@
|
|||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
|
export function isNotesVisibilityForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||||
|
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||||
|
|
||||||
|
if (user.notesVisibility === 'private') return false;
|
||||||
|
if (user.notesVisibility === 'followers' && !user.isFollowing) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||||
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||||
|
|
||||||
|
@ -2030,6 +2030,9 @@ type FollowingUpdateRequest = operations['following___update']['requestBody']['c
|
|||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json'];
|
type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const notesVisibilities: readonly ["public", "followers", "private"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const followingVisibilities: readonly ["public", "followers", "private"];
|
export const followingVisibilities: readonly ["public", "followers", "private"];
|
||||||
|
|
||||||
|
@ -3890,6 +3890,8 @@ export type components = {
|
|||||||
publicReactions: boolean;
|
publicReactions: boolean;
|
||||||
hideActivity: boolean;
|
hideActivity: boolean;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
|
notesVisibility: 'public' | 'followers' | 'private';
|
||||||
|
/** @enum {string} */
|
||||||
followingVisibility: 'public' | 'followers' | 'private';
|
followingVisibility: 'public' | 'followers' | 'private';
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
followersVisibility: 'public' | 'followers' | 'private';
|
followersVisibility: 'public' | 'followers' | 'private';
|
||||||
|
@ -22,6 +22,8 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
|
|||||||
|
|
||||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||||
|
|
||||||
|
export const notesVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
|
|
||||||
export const followingVisibilities = ['public', 'followers', 'private'] as const;
|
export const followingVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
|
|
||||||
export const followersVisibilities = ['public', 'followers', 'private'] as const;
|
export const followersVisibilities = ['public', 'followers', 'private'] as const;
|
||||||
|
@ -19,6 +19,7 @@ export const permissions = consts.permissions;
|
|||||||
export const notificationTypes = consts.notificationTypes;
|
export const notificationTypes = consts.notificationTypes;
|
||||||
export const noteVisibilities = consts.noteVisibilities;
|
export const noteVisibilities = consts.noteVisibilities;
|
||||||
export const mutedNoteReasons = consts.mutedNoteReasons;
|
export const mutedNoteReasons = consts.mutedNoteReasons;
|
||||||
|
export const notesVisibilities = consts.notesVisibilities;
|
||||||
export const followingVisibilities = consts.followingVisibilities;
|
export const followingVisibilities = consts.followingVisibilities;
|
||||||
export const followersVisibilities = consts.followersVisibilities;
|
export const followersVisibilities = consts.followersVisibilities;
|
||||||
export const moderationLogTypes = consts.moderationLogTypes;
|
export const moderationLogTypes = consts.moderationLogTypes;
|
||||||
|
Loading…
Reference in New Issue
Block a user