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

feat: フォローされた際のメッセージを設定できるようにする #14430

Merged
merged 11 commits into from
Sep 28, 2024
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Unreleased

### General
-
- Feat: フォローされた際のメッセージを設定できるように

### Client
-
Expand Down
12 changes: 12 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8629,6 +8629,18 @@ export interface Locale extends ILocale {
* 最大{max}つまでデコレーションを付けられます。
*/
"avatarDecorationMax": ParameterizedString<"max">;
/**
* フォローされた時のメッセージ
*/
"followedMessage": string;
/**
* フォローされた時に相手に表示するメッセージを設定できます。
*/
"followedMessageDescription": string;
/**
* フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。
*/
"followedMessageDescriptionForLockedAccount": string;
};
"_exportOrImport": {
/**
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2273,6 +2273,9 @@ _profile:
changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
followedMessage: "フォローされた時のメッセージ"
followedMessageDescription: "フォローされた時に相手に表示するメッセージを設定できます。"
followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。"

_exportOrImport:
allNotes: "全てのノート"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1723944246767-followedMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class FollowedMessage1723944246767 {
name = 'FollowedMessage1723944246767';

async up(queryRunner) {
await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
}

async down(queryRunner) {
await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
}
}
15 changes: 9 additions & 6 deletions packages/backend/src/core/UserFollowingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,19 @@ export class UserFollowingService implements OnModuleInit {
followeeId: followee.id,
followerId: follower.id,
});

// 通知を作成
if (follower.host === null) {
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
}
}

if (alreadyFollowed) return;

// 通知を作成
if (follower.host === null) {
const profile = await this.cacheService.userProfileCache.fetch(followee.id);

this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
message: profile.followedMessage,
}, followee.id);
}

this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });

const [followeeUser, followerUser] = await Promise.all([
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/activitypub/ApRendererService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ export class ApRendererService {
name: user.name,
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
tag,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/activitypub/misc/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ const extension_context_definition = {
'_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary',
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
'isCat': 'misskey:isCat',
// vcard
vcard: 'http://www.w3.org/2006/vcard/ns#',
Expand Down
12 changes: 7 additions & 5 deletions packages/backend/src/core/activitypub/models/ApPersonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type { ApNoteService } from './ApNoteService.js';
import type { ApMfmService } from '../ApMfmService.js';
import type { ApResolverService, Resolver } from '../ApResolverService.js';
import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports

import type { ApImageService } from './ApImageService.js';
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';

Expand Down Expand Up @@ -307,8 +307,8 @@ export class ApPersonService implements OnModuleInit {
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
}
return 'private';
})
)
}),
),
);

const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
Expand Down Expand Up @@ -370,6 +370,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new MiUserProfile({
userId: user.id,
description: _description,
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
url,
fields,
followingVisibility,
Expand Down Expand Up @@ -494,8 +495,8 @@ export class ApPersonService implements OnModuleInit {
return undefined;
}
return 'private';
})
)
}),
),
);

const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
Expand Down Expand Up @@ -566,6 +567,7 @@ export class ApPersonService implements OnModuleInit {
url,
fields,
description: _description,
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
followingVisibility,
followersVisibility,
birthday: bday?.[0] ?? null,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/activitypub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface IObject {
name?: string | null;
summary?: string;
_misskey_summary?: string;
_misskey_followedMessage?: string | null;
published?: string;
cc?: ApObject;
to?: ApObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
checkValidNotifier?: boolean;
},
Expand Down Expand Up @@ -159,6 +159,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'followRequestAccepted' ? {
message: notification.message,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
Expand Down Expand Up @@ -229,7 +232,7 @@ export class NotificationEntityService implements OnModuleInit {
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
checkValidNotifier?: boolean;
},
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ export class UserEntityService implements OnModuleInit {
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
}))
})),
) : undefined,

...(isDetailed ? {
Expand Down Expand Up @@ -560,13 +560,15 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
followedMessage: relation?.isFollowing ? profile!.followedMessage : undefined,
memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),

...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
followedMessage: profile!.followedMessage,
isModerator: isModerator,
isAdmin: isAdmin,
injectFeaturedNote: profile!.injectFeaturedNote,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type MiNotification = {
id: string;
createdAt: string;
notifierId: MiUser['id'];
message: string | null;
} | {
type: 'roleAssigned';
id: string;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,5 +289,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
export const passwordSchema = { type: 'string', minLength: 1 } as const;
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
8 changes: 8 additions & 0 deletions packages/backend/src/models/UserProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export class MiUserProfile {
})
public description: string | null;

// フォローされた際のメッセージ
@Column('varchar', {
length: 256, nullable: true,
})
public followedMessage: string | null;

// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする

@Column('jsonb', {
default: [],
})
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
format: 'id',
},
message: {
type: 'string',
optional: false, nullable: true,
},
},
}, {
type: 'object',
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ export const packedUserDetailedNotMeOnlySchema = {
ref: 'RoleLite',
},
},
followedMessage: {
type: 'string',
nullable: true, optional: true,
},
memo: {
type: 'string',
nullable: true, optional: false,
Expand Down Expand Up @@ -437,6 +441,10 @@ export const packedMeDetailedOnlySchema = {
nullable: true, optional: false,
format: 'id',
},
followedMessage: {
type: 'string',
nullable: true, optional: false,
},
isModerator: {
type: 'boolean',
nullable: true, optional: false,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/show-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
followedMessage: {
type: 'string',
optional: false, nullable: true,
},
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
Expand Down Expand Up @@ -226,6 +230,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
email: profile.email,
emailVerified: profile.emailVerified,
followedMessage: profile.followedMessage,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/server/api/endpoints/i/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
Expand Down Expand Up @@ -134,6 +133,7 @@ export const paramDef = {
properties: {
name: { ...nameSchema, nullable: true },
description: { ...descriptionSchema, nullable: true },
followedMessage: { ...followedMessageSchema, nullable: true },
location: { ...locationSchema, nullable: true },
birthday: { ...birthdaySchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
Expand Down Expand Up @@ -267,6 +267,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test';

import * as assert from 'assert';
import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';

describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する
Expand Down Expand Up @@ -87,6 +87,7 @@ describe('ユーザー', () => {
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
roles: user.roles,
followedMessage: user.followedMessage,
memo: user.memo,
});
};
Expand Down Expand Up @@ -114,6 +115,7 @@ describe('ユーザー', () => {
...userDetailedNotMe(user),
avatarId: user.avatarId,
bannerId: user.bannerId,
followedMessage: user.followedMessage,
isModerator: user.isModerator,
isAdmin: user.isAdmin,
injectFeaturedNote: user.injectFeaturedNote,
Expand Down Expand Up @@ -350,6 +352,7 @@ describe('ユーザー', () => {
// MeDetailedOnly
assert.strictEqual(response.avatarId, null);
assert.strictEqual(response.bannerId, null);
assert.strictEqual(response.followedMessage, null);
assert.strictEqual(response.isModerator, false);
assert.strictEqual(response.isAdmin, false);
assert.strictEqual(response.injectFeaturedNote, true);
Expand Down Expand Up @@ -413,6 +416,8 @@ describe('ユーザー', () => {
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
{ parameters: () => ({ description: 'x' }) },
{ parameters: () => ({ description: 'My description' }) },
{ parameters: () => ({ followedMessage: null }) },
{ parameters: () => ({ followedMessage: 'Thank you' }) },
{ parameters: () => ({ location: null }) },
{ parameters: () => ({ location: 'x'.repeat(50) }) },
{ parameters: () => ({ location: 'x' }) },
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkFollowButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
<template v-else-if="isFollowing">
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNoteDetailed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
Expand Down
Loading
Loading