Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22.11.0
v22.14.0
34 changes: 34 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,28 @@ export interface Locale extends ILocale {
* ポイント
*/
"point": string;
/**
* {pointName}を送る
*/
"sendPoints": ParameterizedString<"pointName">;
/**
* {name}に{pointName}を送る
*/
"sendPointsTo": ParameterizedString<"name" | "pointName">;
/**
* {name}に{points}{pointName}を送信します。
* よろしいですか?
* ※送信後は取り消すことができません。
*/
"sendPointsConfirm": ParameterizedString<"name" | "points" | "pointName">;
/**
* {pointName}が足りません
*/
"notEnoughPoints": ParameterizedString<"pointName">;
/**
* {pointName}は数字で入力してください
*/
"pointsMustBeNumber": ParameterizedString<"pointName">;
/**
* フォローされています
*/
Expand Down Expand Up @@ -7397,6 +7419,10 @@ export interface Locale extends ILocale {
* ログインボーナスの付与
*/
"loginBonusGrantEnabled": string;
/**
* ポイントの送信
*/
"canSendPoints": string;
/**
* 絵文字ピッカーのプロファイルの上限数(最大5)
*/
Expand Down Expand Up @@ -10042,6 +10068,10 @@ export interface Locale extends ILocale {
* ログインボーナス
*/
"loginbonus": string;
/**
* {sender}から{point}{pointName}をもらいました
*/
"acceptPoints": ParameterizedString<"sender" | "point" | "pointName">;
/**
* 通知テスト
*/
Expand Down Expand Up @@ -10151,6 +10181,10 @@ export interface Locale extends ILocale {
* ログインボーナス
*/
"loginBonus": string;
/**
* {pointName}獲得
*/
"acceptPoints": ParameterizedString<"pointName">;
/**
* エクスポートが完了した
*/
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ notes: "ノート"
following: "フォロー"
followers: "フォロワー"
point: "ポイント"
sendPoints: "{pointName}を送る"
sendPointsTo: "{name}に{pointName}を送る"
sendPointsConfirm: "{name}に{points}{pointName}を送信します。\nよろしいですか?\n※送信後は取り消すことができません。"
notEnoughPoints: "{pointName}が足りません"
pointsMustBeNumber: "{pointName}は数字で入力してください"
followsYou: "フォローされています"
createList: "リスト作成"
manageLists: "リストの管理"
Expand Down Expand Up @@ -1910,6 +1915,7 @@ _role:
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
loginBonusGrantEnabled: "ログインボーナスの付与"
canSendPoints: "ポイントの送信"
emojiPickerProfileLimit: "絵文字ピッカーのプロファイルの上限数(最大5)"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
Expand Down Expand Up @@ -2647,6 +2653,7 @@ _notification:
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
loginbonus: "ログインボーナス"
acceptPoints: "{sender}から{point}{pointName}をもらいました"
testNotification: "通知テスト"
checkNotificationBehavior: "通知の表示を確かめる"
sendTestNotification: "テスト通知を送信する"
Expand Down Expand Up @@ -2676,6 +2683,7 @@ _notification:
roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得"
loginBonus: "ログインボーナス"
acceptPoints: "{pointName}獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
test: "通知のテスト"
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type RolePolicies = {
listPinnedLimit: number;
localTimelineAnyLimit: number;
loginBonusGrantEnabled: boolean;
canSendPoints: boolean;
canImportAntennas: boolean;
canImportBlocking: boolean;
canImportFollowing: boolean;
Expand Down Expand Up @@ -111,6 +112,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
listPinnedLimit: 2,
localTimelineAnyLimit: 3,
loginBonusGrantEnabled: true,
canSendPoints: false,
};

@Injectable()
Expand Down Expand Up @@ -423,6 +425,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
listPinnedLimit: calc('listPinnedLimit', vs => Math.max(...vs)),
localTimelineAnyLimit: calc('localTimelineAnyLimit', vs => Math.max(...vs)),
loginBonusGrantEnabled: calc('loginBonusGrantEnabled', vs => vs.some(v => v === true)),
canSendPoints: calc('canSendPoints', vs => vs.some(v => v === true)),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'loginBonus' ? {
loginBonus: notification.loginBonus,
} : {}),
...(notification.type === 'acceptPoints' ? {
getPoint: notification.getPoint,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export type MiNotification = {
id: string;
createdAt: string;
loginBonus: number;
} | {
type: 'acceptPoints';
id: string;
createdAt: string;
getPoint: number;
notifierId: MiUser['id'];
} | {
type: 'pollEnded';
id: string;
Expand Down
24 changes: 24 additions & 0 deletions packages/backend/src/models/json-schema/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,30 @@ export const packedNotificationSchema = {
},
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['acceptPoints'],
},
getPoint: {
type: 'number',
optional: false, nullable: false,
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
}, {
type: 'object',
properties: {
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canSendPoints: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/server/api/endpoint-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,4 @@ export * as 'admin/inbox-rule/delete' from './endpoints/admin/inbox-rule/delete.
export * as 'admin/inbox-rule/list' from './endpoints/admin/inbox-rule/list.js';
export * as 'users/lists/list-favorite' from './endpoints/users/lists/list-favorite.js';
export * as 'notes/home-local-timeline' from './endpoints/notes/home-local-timeline.js'
export * as 'point/send' from './endpoints/point/send.js';
102 changes: 102 additions & 0 deletions packages/backend/src/server/api/endpoints/point/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';

export const meta = {
tag: ['point'],
requireCredential: true,
kind: 'write:points',
secure: true,
errors: {
userIsNotFound: {
message: 'user is not found.',
code: 'USER_IS_NOT_FOUND',
id: '87472165-2e39-fcb9-352b-98c24a6d825e',
},
notEnoughPoints: {
message: 'not enough points.',
code: 'NOT_ENOUGH_POINTS',
id: '10e9f46d-9f1f-7a4b-801b-5fe88e4bb1aa',
},
cannotSendPoints: {
message: 'cannot send points.',
code: 'CANNOT_SEND_POINTS',
id: 'f1cf2616-db7b-3f97-5a14-06a0a0005f8f',
},
recipientIsYou: {
message: 'recipient is you.',
code: 'RECIPIENT_IS_ME',
id: '24095319-69ee-8033-b0a2-9ad6928bbcb8',
},
},

} as const;

export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
points: { type: 'number' },
},
required: ['userId', 'points'],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private notificationService: NotificationService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const sender = await this.usersRepository.findOneBy({ id: me.id });
const user = await this.usersRepository.findOneBy({ id: ps.userId });

if (sender == null || user == null) {
throw new ApiError(meta.errors.userIsNotFound);
}

if ((await this.roleService.getUserPolicies(sender.id)).canSendPoints === false) {
throw new ApiError(meta.errors.cannotSendPoints);
}
//送れるかどうかチェック
if (sender.getPoints < ps.points) {
throw new ApiError(meta.errors.notEnoughPoints);
}

// 受信者が自分ならエラー
if (sender.id === user.id) {
throw new ApiError(meta.errors.recipientIsYou);
}

// 送るポイントが0以下の場合はエラー
if (ps.points <= 0) {
throw new ApiError(meta.errors.cannotSendPoints);
}

//ポイントを送る
await this.usersRepository.update(sender.id, {
getPoints: sender.getPoints - ps.points,
});
await this.usersRepository.update(user.id, {
getPoints: user.getPoints + ps.points,
});

this.notificationService.createNotification(user.id, 'acceptPoints', {
getPoint: ps.points,
}, sender.id);

return {};
});
}
}
1 change: 1 addition & 0 deletions packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const notificationTypes = [
'app',
'test',
'loginBonus',
'acceptPoints',
] as const;

export const groupedNotificationTypes = [
Expand Down
1 change: 1 addition & 0 deletions packages/frontend-shared/js/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const ROLE_POLICIES = [
'listPinnedLimit',
'localTimelineAnyLimit',
'loginBonusGrantEnabled',
'canSendPoints',
'canImportAntennas',
'canImportBlocking',
'canImportFollowing',
Expand Down
33 changes: 29 additions & 4 deletions packages/frontend/src/components/MkNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
v-else-if="notification.type === 'renote:grouped'"

Check failure on line 18 in packages/frontend/src/components/MkNotification.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain
:class="[$style.icon, $style.icon_renoteGroup]"
>
<i class="ti ti-repeat" style="line-height: 1"></i>
</div>
<img
v-else-if="notification.type === 'test'"

Check failure on line 24 in packages/frontend/src/components/MkNotification.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain
:class="$style.icon"
:src="infoImageUrl"
/>
<MkAvatar
v-else-if="'user' in notification"

Check failure on line 29 in packages/frontend/src/components/MkNotification.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain
:class="$style.icon"
:user="notification.user"
link
Expand Down Expand Up @@ -55,9 +55,10 @@
[$style.t_achievementEarned]:
notification.type === 'achievementEarned' || notification.type === 'loginBonus',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
[$style.t_createToken]: notification.type === 'createToken',
[$style.t_roleAssigned]:
[$style.t_login]: notification.type === 'login',
[$style.t_acceptPoints]: notification.type === 'acceptPoints',
[$style.t_createToken]: notification.type === 'createToken',
[$style.t_roleAssigned]:
notification.type === 'roleAssigned' &&
notification.role.iconUrl == null,
},
Expand Down Expand Up @@ -91,6 +92,10 @@
v-else-if="notification.type === 'loginBonus'"
class="ti ti-medal"
></i>
<i
v-else-if="notification.type === 'acceptPoints'"
class="ti ti-medal"
></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
<i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i>
Expand Down Expand Up @@ -143,7 +148,8 @@
notification.type === 'quote' ||
notification.type === 'reaction' ||
notification.type === 'receiveFollowRequest' ||
notification.type === 'followRequestAccepted'
notification.type === 'followRequestAccepted' ||
notification.type === 'acceptPoints'
"
v-user-preview="notification.user.id"
:class="$style.headerName"
Expand Down Expand Up @@ -301,6 +307,18 @@
})
}}
</div>
<div
v-else-if="notification.type === 'acceptPoints'"
:class="$style.text"
>
{{
i18n.tsx._notification.acceptPoints({
point: notification.getPoint,
pointName: instance?.pointName ?? i18n.ts.point,
sender: notification.user.username,
})
}}
</div>
<MkA
v-else-if="notification.type === 'achievementEarned'"
:class="$style.text"
Expand Down Expand Up @@ -494,6 +512,7 @@
--eventReactionHeart: var(--MI_THEME-love);
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventAcceptPoints: #cb9a11;
--eventLogin: #007aff;
--eventOther: #88a6b7;
}
Expand Down Expand Up @@ -618,6 +637,12 @@
pointer-events: none;
}

.t_acceptPoints {
padding: 3px;
background: var(--eventAcceptPoints);
pointer-events: none;
}

.t_login {
padding: 3px;
background: var(--eventLogin);
Expand Down
Loading
Loading