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: Misskey Gamesのプレイ可否をロールで制限できるように #14955

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Feat: Misskey Gamesのプレイ可否をロールで設定可能に
- Enhance: 依存関係の更新
- Enhance: l10nの更新

Expand Down
16 changes: 16 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5218,6 +5218,10 @@ export interface Locale extends ILocale {
* 利用可能なロール
*/
"availableRoles": string;
/**
* このアカウントにはMisskey Gamesをプレイする権限がありません。
*/
"youCannotPlayGames": string;
"_accountSettings": {
/**
* コンテンツの表示にログインを必須にする
Expand Down Expand Up @@ -6989,6 +6993,10 @@ export interface Locale extends ILocale {
* リストのインポートを許可
*/
"canImportUserLists": string;
/**
* Misskey Gamesの利用
*/
"canPlayGames": string;
};
"_condition": {
/**
Expand Down Expand Up @@ -10406,6 +10414,14 @@ export interface Locale extends ILocale {
* 石をアイコンにする
*/
"useAvatarAsStone": string;
/**
* 相手のユーザーにはMisskey Gamesをプレイする権限がありません。
*/
"targetUserIsNotAvailable": string;
/**
* 相手に自分自身を指定することはできません。
*/
"targetIsYourself": string;
};
"_offlineScreen": {
/**
Expand Down
4 changes: 4 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示に
lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
youCannotPlayGames: "このアカウントではMisskey Gamesをプレイできません。"

_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
Expand Down Expand Up @@ -1806,6 +1807,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
canPlayGames: "Misskey Gamesの利用"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
Expand Down Expand Up @@ -2767,6 +2769,8 @@ _reversi:
disallowIrregularRules: "変則なし"
showBoardLabels: "盤面に行・列番号を表示"
useAvatarAsStone: "石をアイコンにする"
targetUserIsNotAvailable: "相手はMisskey Gamesをプレイできません。"
targetIsYourself: "相手に自分自身を指定することはできません。"

_offlineScreen:
title: "オフライン - サーバーに接続できません"
Expand Down
17 changes: 16 additions & 1 deletion packages/backend/src/core/ReversiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleService } from '@/core/RoleService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';

const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
Expand All @@ -41,6 +43,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
private reversiGamesRepository: ReversiGamesRepository,

private cacheService: CacheService,
private roleService: RoleService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private reversiGameEntityService: ReversiGameEntityService,
Expand Down Expand Up @@ -93,7 +96,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) {
throw new Error('You cannot match yourself.');
throw new IdentifiableError('eeb95261-6538-4294-a1d1-ed9a40d2c25b', 'You cannot match with yourself.');
}

const myPolicy = await this.roleService.getUserPolicies(me.id);
const targetPolicy = await this.roleService.getUserPolicies(targetUser.id);

if (!myPolicy.canPlayGames || !targetPolicy.canPlayGames) {
throw new IdentifiableError('6a8a09eb-f359-4339-9b1d-2fb3f8c0df45', 'You or target user is not available due to server policy.');
}

if (!multiple) {
Expand Down Expand Up @@ -143,6 +153,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {

@bindThis
public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> {
const myPolicy = await this.roleService.getUserPolicies(me.id);
if (!myPolicy.canPlayGames) {
throw new IdentifiableError('6a8a09eb-f359-4339-9b1d-2fb3f8c0df45', 'You cannot play due to server policy.');
}

if (!multiple) {
// 既にマッチしている対局が無いか探す(3分以内)
const games = await this.reversiGamesRepository.find({
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 @@ -63,6 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
canPlayGames: boolean;
};

export const DEFAULT_POLICIES: RolePolicies = {
Expand Down Expand Up @@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
canPlayGames: true,
};

@Injectable()
Expand Down Expand Up @@ -402,6 +404,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
canPlayGames: calc('canPlayGames', vs => vs.some(v => v === true)),
};
}

Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/misc/prelude/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,8 @@ export function toArray<T>(x: T | T[] | undefined): T[] {
export function toSingle<T>(x: T | T[] | undefined): T | undefined {
return Array.isArray(x) ? x[0] : x;
}

export async function asyncFilter<T>(xs: T[], f: (x: T) => Promise<boolean>): Promise<T[]> {
const bits = await Promise.all(xs.map(f));
return xs.filter((_, i) => bits[i]);
}
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 @@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canPlayGames: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ApiError } from '../../error.js';

export const meta = {
requireCredential: true,
requireRolePolicy: 'canPlayGames',

kind: 'write:account',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ReversiService } from '@/core/ReversiService.js';

export const meta = {
requireCredential: true,
requireRolePolicy: 'canPlayGames',

kind: 'write:account',

Expand Down
9 changes: 8 additions & 1 deletion packages/backend/src/server/api/endpoints/reversi/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { DI } from '@/di-symbols.js';
import type { ReversiGamesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { RoleService } from '@/core/RoleService.js';
import type { ReversiGamesRepository } from '@/models/_.js';

export const meta = {
requireCredential: false,
Expand Down Expand Up @@ -39,6 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private reversiGamesRepository: ReversiGamesRepository,

private reversiGameEntityService: ReversiGameEntityService,
private roleService: RoleService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
Expand All @@ -52,6 +54,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.where('game.user1Id = :userId', { userId: me.id })
.orWhere('game.user2Id = :userId', { userId: me.id });
}));
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.canPlayGames) {
// 今ゲームをプレイできない場合は終了済みのゲームのみ表示
query.andWhere('game.isEnded = TRUE');
}
} else {
query.andWhere('game.isStarted = TRUE');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ReversiService } from '@/core/ReversiService.js';
import { asyncFilter } from '@/misc/prelude/array.js';

export const meta = {
requireCredential: true,
requireRolePolicy: 'canPlayGames',

kind: 'read:account',

Expand All @@ -28,10 +31,14 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private userEntityService: UserEntityService,
private roleService: RoleService,
private reversiService: ReversiService,
) {
super(meta, paramDef, async (ps, me) => {
const invitations = await this.reversiService.getInvitations(me);
const invitations = await asyncFilter(await this.reversiService.getInvitations(me), async (userId) => {
const policies = await this.roleService.getUserPolicies(userId);
return policies.canPlayGames;
});

return await this.userEntityService.packMany(invitations, me);
});
Expand Down
34 changes: 29 additions & 5 deletions packages/backend/src/server/api/endpoints/reversi/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RoleService } from '@/core/RoleService.js';
import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { ApiError } from '../../error.js';
import { GetterService } from '../../GetterService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';

export const meta = {
requireCredential: true,
requireRolePolicy: 'canPlayGames',

kind: 'write:account',

Expand All @@ -27,6 +30,12 @@ export const meta = {
code: 'TARGET_IS_YOURSELF',
id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e',
},

isNotAvailable: {
message: 'You or target user is not available due to server policy.',
code: 'TARGET_IS_NOT_AVAILABLE',
id: '3a8a677f-98e5-4c4d-b059-e5874b44bd4f',
},
},

res: {
Expand Down Expand Up @@ -61,13 +70,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
}) : null;

const game = target
? await this.reversiService.matchSpecificUser(me, target, ps.multiple)
: await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple);
try {
const game = target
? await this.reversiService.matchSpecificUser(me, target, ps.multiple)
: await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple);

if (game == null) return;
if (game == null) return;

return await this.reversiGameEntityService.packDetail(game);
return await this.reversiGameEntityService.packDetail(game);
} catch (err) {
if (err instanceof IdentifiableError) {
switch (err.id) {
case 'eeb95261-6538-4294-a1d1-ed9a40d2c25b':
throw new ApiError(meta.errors.isYourself);
case '6a8a09eb-f359-4339-9b1d-2fb3f8c0df45':
throw new ApiError(meta.errors.isNotAvailable);
default:
throw err;
}
} else {
throw err;
}
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ApiError } from '../../error.js';

export const meta = {
requireCredential: true,
requireRolePolicy: 'canPlayGames',

kind: 'write:account',

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 @@ -106,6 +106,7 @@ export const ROLE_POLICIES = [
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
'canPlayGames',
] as const;

// なんか動かない
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/MkSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ watch(modelValue, () => {
}, { immediate: true });

function show() {
if (opening.value) return;
if (opening.value || props.disabled || props.readonly) return;
focus();

opening.value = true;
Expand Down Expand Up @@ -233,7 +233,7 @@ function show() {
outline: none;
}

&:hover {
&:hover:not(.disabled) {
> .inputCore {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/components/MkSwitch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const toggle = () => {
&.disabled {
opacity: 0.6;
cursor: not-allowed;

.label {
cursor: not-allowed;
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions packages/frontend/src/pages/admin/roles.editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</div>
</MkFolder>

<MkFolder v-if="matchQuery([i18n.ts._role._options.canPlayGames, 'canPlayGames'])">
<template #label>{{ i18n.ts._role._options.canPlayGames }}</template>
<template #suffix>
<span v-if="role.policies.canPlayGames.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canPlayGames.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canPlayGames)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canPlayGames.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canPlayGames.value" :disabled="role.policies.canPlayGames.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canPlayGames.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
</div>
</FormSlot>
</div>
Expand Down
8 changes: 8 additions & 0 deletions packages/frontend/src/pages/admin/roles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>

<MkFolder v-if="matchQuery([i18n.ts._role._options.canPlayGames, 'canPlayGames'])">
<template #label>{{ i18n.ts._role._options.canPlayGames }}</template>
<template #suffix>{{ policies.canPlayGames ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canPlayGames">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
</div>
</MkFolder>
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
Expand Down
Loading
Loading