mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-03-13 20:57:58 +01:00
feat: Misskey Gamesのプレイ可否をロールで制限できるように
This commit is contained in:
parent
4d54101510
commit
eec30992ce
21 changed files with 153 additions and 19 deletions
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -5218,6 +5218,10 @@ export interface Locale extends ILocale {
|
|||
* 利用可能なロール
|
||||
*/
|
||||
"availableRoles": string;
|
||||
/**
|
||||
* このアカウントにはMisskey Gamesをプレイする権限がありません。
|
||||
*/
|
||||
"youCannotPlayGames": string;
|
||||
"_accountSettings": {
|
||||
/**
|
||||
* コンテンツの表示にログインを必須にする
|
||||
|
@ -6989,6 +6993,10 @@ export interface Locale extends ILocale {
|
|||
* リストのインポートを許可
|
||||
*/
|
||||
"canImportUserLists": string;
|
||||
/**
|
||||
* Misskey Gamesの利用
|
||||
*/
|
||||
"canPlayGames": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
|
|
|
@ -1300,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示に
|
|||
lockdown: "ロックダウン"
|
||||
pleaseSelectAccount: "アカウントを選択してください"
|
||||
availableRoles: "利用可能なロール"
|
||||
youCannotPlayGames: "このアカウントにはMisskey Gamesをプレイする権限がありません。"
|
||||
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||
|
@ -1806,6 +1807,7 @@ _role:
|
|||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
canPlayGames: "Misskey Gamesの利用"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
|
|
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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({
|
||||
|
|
|
@ -63,6 +63,7 @@ export type RolePolicies = {
|
|||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canPlayGames: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canPlayGames: true,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canPlayGames: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ApiError } from '../../error.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canPlayGames',
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ReversiService } from '@/core/ReversiService.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canPlayGames',
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ApiError } from '../../error.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canPlayGames',
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ export const ROLE_POLICIES = [
|
|||
'canImportFollowing',
|
||||
'canImportMuting',
|
||||
'canImportUserLists',
|
||||
'canPlayGames',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
|
|
@ -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;
|
||||
|
@ -233,7 +233,7 @@ function show() {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.disabled) {
|
||||
> .inputCore {
|
||||
border-color: var(--MI_THEME-inputBorderHover) !important;
|
||||
}
|
||||
|
|
|
@ -66,6 +66,10 @@ const toggle = () => {
|
|||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
.label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -23,20 +23,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<MkSelect v-model="gameMode">
|
||||
<MkInfo v-if="$i && !$i.policies.canPlayGames" warn>{{ i18n.ts.youCannotPlayGames }}</MkInfo>
|
||||
<MkSelect :disabled="$i == null || !$i.policies.canPlayGames" v-model="gameMode">
|
||||
<option value="normal">NORMAL</option>
|
||||
<option value="square">SQUARE</option>
|
||||
<option value="yen">YEN</option>
|
||||
<option value="sweets">SWEETS</option>
|
||||
<!--<option value="space">SPACE</option>-->
|
||||
</MkSelect>
|
||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
<MkButton primary gradate large rounded inline :disabled="$i == null || !$i.policies.canPlayGames" @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
<MkSwitch v-model="mute">
|
||||
<MkSwitch :disabled="$i == null || !$i.policies.canPlayGames" v-model="mute">
|
||||
<template #label>{{ i18n.ts.mute }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
|
@ -90,7 +91,9 @@ import { computed, ref, watch } from 'vue';
|
|||
import XGame from './drop-and-fusion.game.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
|
|
|
@ -181,7 +181,7 @@ const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
|
|||
}));
|
||||
|
||||
const iAmPlayer = computed(() => {
|
||||
return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
|
||||
return $i.policies.canPlayGames && (game.value.user1Id === $i.id || game.value.user2Id === $i.id);
|
||||
});
|
||||
|
||||
const myColor = computed(() => {
|
||||
|
|
|
@ -11,14 +11,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div class="_panel _gaps" style="padding: 16px;">
|
||||
<MkInfo v-if="$i && !$i.policies.canPlayGames" warn>{{ i18n.ts.youCannotPlayGames }}</MkInfo>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton>
|
||||
<MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton>
|
||||
<MkButton primary gradate rounded :disabled="$i == null || !$i.policies.canPlayGames" @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton>
|
||||
<MkButton primary gradate rounded :disabled="$i == null || !$i.policies.canPlayGames" @click="matchUser">{{ i18n.ts.invite }}</MkButton>
|
||||
</div>
|
||||
<div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
</div>
|
||||
|
||||
<MkFolder v-if="invitations.length > 0" :defaultOpen="true">
|
||||
<MkFolder v-if="$i && $i.policies.canPlayGames && invitations.length > 0" :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.invitations }}</template>
|
||||
<div class="_gaps_s">
|
||||
<button v-for="user in invitations" :key="user.id" v-panel :class="$style.invitation" class="_button" tabindex="-1" @click="accept(user)">
|
||||
|
@ -112,6 +113,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
@ -176,7 +178,7 @@ async function matchHeatbeat() {
|
|||
if (matchingUser.value) {
|
||||
const res = await misskeyApi('reversi/match', {
|
||||
userId: matchingUser.value.id,
|
||||
});
|
||||
}).catch(onApiError);
|
||||
|
||||
if (res != null) {
|
||||
startGame(res);
|
||||
|
@ -185,7 +187,7 @@ async function matchHeatbeat() {
|
|||
const res = await misskeyApi('reversi/match', {
|
||||
userId: null,
|
||||
noIrregularRules: noIrregularRules.value,
|
||||
});
|
||||
}).catch(onApiError);
|
||||
|
||||
if (res != null) {
|
||||
startGame(res);
|
||||
|
@ -193,6 +195,21 @@ async function matchHeatbeat() {
|
|||
}
|
||||
}
|
||||
|
||||
function onApiError(err) {
|
||||
if (err.id === '7f86f06f-7e15-4057-8561-f4b6d4ac755a') {
|
||||
// Role permission error
|
||||
matchingUser.value = null;
|
||||
matchingAny.value = false;
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.permissionDeniedError,
|
||||
text: i18n.ts.permissionDeniedErrorDescription,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function matchUser() {
|
||||
pleaseLogin();
|
||||
|
||||
|
@ -248,6 +265,8 @@ useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
|
|||
onMounted(() => {
|
||||
misskeyApi('reversi/invitations').then(_invitations => {
|
||||
invitations.value = _invitations;
|
||||
}).catch(() => {
|
||||
invitations.value = [];
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', cancelMatching);
|
||||
|
|
|
@ -4872,6 +4872,7 @@ export type components = {
|
|||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canPlayGames: boolean;
|
||||
};
|
||||
ReversiGameLite: {
|
||||
/** Format: id */
|
||||
|
|
Loading…
Add table
Reference in a new issue