spec(role/ScheduledNote): ロールで予約投稿の個数・予約の最大日数を制御できるように (MisskeyIO#906)
This commit is contained in:
parent
f0f86f1121
commit
8821e3e81b
@ -1794,6 +1794,8 @@ _role:
|
||||
ltlAvailable: "Can view the local timeline"
|
||||
canPublicNote: "Can send public notes"
|
||||
canScheduleNote: "Can schedule notes"
|
||||
scheduleNoteLimit: "Maximum number of scheduled notes"
|
||||
scheduleNoteMaxDays: "Maximum number of days that note can be scheduled"
|
||||
canInitiateConversation: "Can mention, reply or quote"
|
||||
canCreateContent: "Can create contents"
|
||||
canUpdateContent: "Can edit contents"
|
||||
@ -2324,6 +2326,7 @@ _postForm:
|
||||
d: "What do you want to say?"
|
||||
e: "Start writing..."
|
||||
f: "Waiting for you to write..."
|
||||
policyScheduleNoteMaxDaysExceeded: "The maximum number of days you can schedule notes for with your current support plan is {max}.\nYou can upgrade your plan [here](https://go.misskey.io/donate)."
|
||||
tosAndGuidelinesInfo: "Before posting, please read the [Terms of Service]({tosUrl}) and [NSFW Guidelines](https://go.misskey.io/media-guideline)."
|
||||
_profile:
|
||||
name: "Name"
|
||||
|
13
locales/index.d.ts
vendored
13
locales/index.d.ts
vendored
@ -7019,6 +7019,14 @@ export interface Locale extends ILocale {
|
||||
* 予約投稿の許可
|
||||
*/
|
||||
"canScheduleNote": string;
|
||||
/**
|
||||
* 予約投稿の最大数
|
||||
*/
|
||||
"scheduleNoteLimit": string;
|
||||
/**
|
||||
* 予約投稿の最大日数
|
||||
*/
|
||||
"scheduleNoteMaxDays": string;
|
||||
/**
|
||||
* メンション、リプライ、引用の許可
|
||||
*/
|
||||
@ -9067,6 +9075,11 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"f": string;
|
||||
};
|
||||
/**
|
||||
* 現在の支援プランで予約できる日数の上限は{max}日です。
|
||||
* [ここ](https://go.misskey.io/donate)からプランをアップグレードできます。
|
||||
*/
|
||||
"policyScheduleNoteMaxDaysExceeded": ParameterizedString<"max">;
|
||||
/**
|
||||
* 投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。
|
||||
*/
|
||||
|
@ -1808,6 +1808,8 @@ _role:
|
||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
canScheduleNote: "予約投稿の許可"
|
||||
scheduleNoteLimit: "予約投稿の最大数"
|
||||
scheduleNoteMaxDays: "予約投稿の最大日数"
|
||||
canInitiateConversation: "メンション、リプライ、引用の許可"
|
||||
canCreateContent: "コンテンツの作成"
|
||||
canUpdateContent: "コンテンツの編集"
|
||||
@ -2377,6 +2379,7 @@ _postForm:
|
||||
d: "言いたいことは?"
|
||||
e: "ここに書いてください"
|
||||
f: "あなたが書くのを待っています..."
|
||||
policyScheduleNoteMaxDaysExceeded: "現在の支援プランで予約できる日数の上限は{max}日です。\n[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
|
||||
tosAndGuidelinesInfo: "投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。"
|
||||
|
||||
_profile:
|
||||
|
@ -1791,6 +1791,8 @@ _role:
|
||||
ltlAvailable: "로컬 타임라인 보이기"
|
||||
canPublicNote: "공개 노트 허용"
|
||||
canScheduleNote: "노트 예약 허용"
|
||||
scheduleNoteLimit: "노트 예약 한도"
|
||||
scheduleNoteMaxDays: "노트 예약 최대 일수"
|
||||
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
||||
canCreateContent: "컨텐츠 생성 허용"
|
||||
canUpdateContent: "컨텐츠 수정 허용"
|
||||
@ -2310,6 +2312,7 @@ _postForm:
|
||||
d: "말하고 싶은 게 있나요?"
|
||||
e: "여기에 적어 주세요"
|
||||
f: "글 쓰기를 기다려요…"
|
||||
policyScheduleNoteMaxDaysExceeded: "현재 지원 플랜의 예약 가능한 최대 일수는 {max}일입니다.\n[여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
|
||||
tosAndGuidelinesInfo: "노트를 게시하기 전에 [이용약관]({tosUrl})과 [NSFW 가이드라인](https://go.misskey.io/media-guideline)을 반드시 읽어 주세요."
|
||||
_profile:
|
||||
name: "이름"
|
||||
|
@ -425,6 +425,15 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
throw new IdentifiableError('7cc42034-f7ab-4f7c-87b4-e00854479080', 'User has no permission to schedule notes.');
|
||||
}
|
||||
|
||||
if ((data.scheduledAt.getTime() - Date.now()) / 86_400_000 > policies.scheduleNoteMaxDays) {
|
||||
throw new IdentifiableError('506006cf-3092-4ae1-8145-b025001c591f', `User can schedule notes up to ${policies.scheduleNoteMaxDays} days in the future.`);
|
||||
}
|
||||
|
||||
const scheduledCount = await this.scheduledNotesRepository.countBy({ userId: user.id });
|
||||
if (scheduledCount >= policies.scheduleNoteLimit) {
|
||||
throw new IdentifiableError('7fc78d25-d947-45c1-9547-02257b98cab3', `User can schedule up to ${policies.scheduleNoteLimit} notes.`);
|
||||
}
|
||||
|
||||
const draft = await this.insertScheduledNote(user, data);
|
||||
|
||||
await this.queueService.createScheduledNoteJob(draft.id, draft.scheduledAt!);
|
||||
|
@ -37,6 +37,8 @@ export type RolePolicies = {
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canScheduleNote: boolean;
|
||||
scheduleNoteLimit: number;
|
||||
scheduleNoteMaxDays: number;
|
||||
canInitiateConversation: boolean;
|
||||
canCreateContent: boolean;
|
||||
canUpdateContent: boolean;
|
||||
@ -79,6 +81,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
canScheduleNote: true,
|
||||
scheduleNoteLimit: 10,
|
||||
scheduleNoteMaxDays: 365,
|
||||
canInitiateConversation: true,
|
||||
canCreateContent: true,
|
||||
canUpdateContent: true,
|
||||
@ -392,6 +396,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
canScheduleNote: calc('canScheduleNote', vs => vs.some(v => v === true)),
|
||||
scheduleNoteLimit: calc('scheduleNoteLimit', vs => Math.max(...vs)),
|
||||
scheduleNoteMaxDays: calc('scheduleNoteMaxDays', vs => Math.max(...vs)),
|
||||
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
||||
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
||||
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
||||
|
@ -184,6 +184,14 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
scheduleNoteLimit: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
scheduleNoteMaxDays: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canInitiateConversation: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -155,18 +155,26 @@ export const meta = {
|
||||
id: 'e577d185-8179-4a17-b47f-6093985558e6',
|
||||
},
|
||||
|
||||
cannotScheduleToFarFuture: {
|
||||
message: 'Cannot schedule to the far future.',
|
||||
code: 'CANNOT_SCHEDULE_TO_FAR_FUTURE',
|
||||
id: 'ea102856-e8da-4ae9-a98a-0326821bd177',
|
||||
},
|
||||
|
||||
cannotScheduleSameTime: {
|
||||
message: 'Cannot schedule multiple notes at the same time.',
|
||||
code: 'CANNOT_SCHEDULE_SAME_TIME',
|
||||
id: '187a8fab-fd83-4ae6-a46c-0f6f07784634',
|
||||
},
|
||||
|
||||
tooManyScheduledNotes: {
|
||||
message: 'You cannot schedule notes any more.',
|
||||
code: 'TOO_MANY_SCHEDULED_NOTES',
|
||||
kind: 'permission',
|
||||
id: '9e33041f-f6fb-414d-98c1-591466e55287'
|
||||
},
|
||||
|
||||
cannotScheduleToFarFuture: {
|
||||
message: 'Cannot schedule to the far future.',
|
||||
code: 'CANNOT_SCHEDULE_TO_FAR_FUTURE',
|
||||
kind: 'permission',
|
||||
id: 'ea102856-e8da-4ae9-a98a-0326821bd177',
|
||||
},
|
||||
|
||||
rolePermissionDenied: {
|
||||
message: 'You are not assigned to a required role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
@ -462,11 +470,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
logger.error('Cannot schedule to the past.', { scheduledAt });
|
||||
throw new ApiError(meta.errors.cannotScheduleToPast);
|
||||
}
|
||||
|
||||
if (scheduledAt.getTime() - now.getTime() > ms('1year')) {
|
||||
logger.error('Cannot schedule to the far future.', { scheduledAt });
|
||||
throw new ApiError(meta.errors.cannotScheduleToFarFuture);
|
||||
}
|
||||
}
|
||||
|
||||
// 投稿を作成
|
||||
@ -517,10 +520,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
logger.error('Failed to create a note.', { error: err });
|
||||
|
||||
if (err instanceof IdentifiableError) {
|
||||
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') throw new ApiError(meta.errors.containsTooManyMentions);
|
||||
if (err.id === '7cc42034-f7ab-4f7c-87b4-e00854479080') throw new ApiError(meta.errors.rolePermissionDenied);
|
||||
if (err.id === '5ea8e4f5-9d64-4e6c-92b8-9e2b5a4756bc') throw new ApiError(meta.errors.cannotScheduleSameTime);
|
||||
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords, { message: err.message });
|
||||
if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') throw new ApiError(meta.errors.containsTooManyMentions, { message: err.message });
|
||||
if (err.id === '5ea8e4f5-9d64-4e6c-92b8-9e2b5a4756bc') throw new ApiError(meta.errors.cannotScheduleSameTime, { message: err.message });
|
||||
if (err.id === '7fc78d25-d947-45c1-9547-02257b98cab3') throw new ApiError(meta.errors.tooManyScheduledNotes, { message: err.message });
|
||||
if (err.id === '506006cf-3092-4ae1-8145-b025001c591f') throw new ApiError(meta.errors.cannotScheduleToFarFuture, { message: err.message });
|
||||
if (err.id === '7cc42034-f7ab-4f7c-87b4-e00854479080') throw new ApiError(meta.errors.rolePermissionDenied, { message: err.message });
|
||||
}
|
||||
|
||||
throw err;
|
||||
|
@ -82,8 +82,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="mk-input-text" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<div v-if="scheduledTime" :class="$style.scheduledTime">
|
||||
<div><i class="ti ti-calendar-clock"></i></div>
|
||||
<span>{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}</span>
|
||||
<div>
|
||||
<div style="display: flex; gap: 4px" :style="scheduledTimeExceededPolicy ? 'color: var(--error)' : undefined">
|
||||
<span style="margin-right: 4px"><i class="ti ti-calendar-clock"></i></span>
|
||||
<component :is="scheduledTimeExceededPolicy ? 'del' : 'span'" :style="scheduledTimeExceededPolicy ? 'opacity: 0.6' : undefined">
|
||||
{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
|
||||
</component>
|
||||
</div>
|
||||
<div v-if="scheduledTimeExceededPolicy" style="display: flex; gap: 4px; margin-top: 4px; color: var(--infoWarnFg)">
|
||||
<span style="margin-right: 4px"><i class="ti ti-exclamation-circle"></i></span>
|
||||
<Mfm :text="i18n.tsx._postForm.policyScheduleNoteMaxDaysExceeded({ max: $i.policies.scheduleNoteMaxDays })"/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="_button" style="margin-left: auto" @click="scheduledTime = null"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkInfo v-if="files.length > 0 && instance.tosUrl" warn style="margin-top: 8px;" :rounded="false">
|
||||
@ -219,6 +229,9 @@ if (props.initialVisibleUsers) {
|
||||
}
|
||||
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
||||
const scheduledTime = ref<Date | null>(null);
|
||||
const scheduledTimeExceededPolicy = computed(() =>
|
||||
scheduledTime.value ? (scheduledTime.value.getTime() - Date.now()) / 86_400_000 > $i!.policies.scheduleNoteMaxDays : false
|
||||
);
|
||||
const autocompleteTextareaInput = ref<Autocomplete | null>(null);
|
||||
const autocompleteCwInput = ref<Autocomplete | null>(null);
|
||||
const autocompleteHashtagsInput = ref<Autocomplete | null>(null);
|
||||
@ -285,16 +298,20 @@ const maxTextLength = computed((): number => {
|
||||
});
|
||||
|
||||
const canPost = computed((): boolean => {
|
||||
return !props.mock && !posting.value && !posted.value &&
|
||||
(
|
||||
return !props.mock
|
||||
&& !posting.value
|
||||
&& !posted.value
|
||||
&& (
|
||||
1 <= textLength.value ||
|
||||
1 <= files.value.length ||
|
||||
poll.value != null ||
|
||||
renote.value != null ||
|
||||
(reply.value != null && quoteId.value != null)
|
||||
) &&
|
||||
(textLength.value <= maxTextLength.value) &&
|
||||
(!poll.value || poll.value.choices.length >= 2);
|
||||
)
|
||||
&& (textLength.value <= maxTextLength.value)
|
||||
&& (!poll.value || poll.value.choices.length >= 2)
|
||||
&& !scheduledTimeExceededPolicy.value
|
||||
;
|
||||
});
|
||||
|
||||
const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
|
||||
@ -1393,8 +1410,10 @@ defineExpose({
|
||||
|
||||
.scheduledTime {
|
||||
display: flex;
|
||||
padding: 8px 24px;
|
||||
padding: 8px 12px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-size: 90%;
|
||||
background: var(--infoBg);
|
||||
}
|
||||
|
||||
|
@ -79,6 +79,8 @@ export const ROLE_POLICIES = [
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'canScheduleNote',
|
||||
'scheduleNoteLimit',
|
||||
'scheduleNoteMaxDays',
|
||||
'canInitiateConversation',
|
||||
'canCreateContent',
|
||||
'canUpdateContent',
|
||||
|
@ -185,6 +185,44 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteLimit, 'scheduleNoteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.scheduleNoteLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.scheduleNoteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.scheduleNoteLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduleNoteLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.scheduleNoteLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.scheduleNoteLimit.value" :disabled="role.policies.scheduleNoteLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.scheduleNoteLimit.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>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMaxDays, 'scheduleNoteMaxDays'])">
|
||||
<template #label>{{ i18n.ts._role._options.scheduleNoteMaxDays }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.scheduleNoteMaxDays.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.scheduleNoteMaxDays.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduleNoteMaxDays)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.scheduleNoteMaxDays.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.scheduleNoteMaxDays.value" :disabled="role.policies.scheduleNoteMaxDays.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.scheduleNoteMaxDays.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>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||
<template #suffix>
|
||||
|
@ -56,6 +56,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteLimit, 'scheduleNoteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.scheduleNoteLimit }}</template>
|
||||
<template #suffix>{{ policies.scheduleNoteLimit }}</template>
|
||||
<MkInput v-model="policies.scheduleNoteLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMaxDays, 'scheduleNoteMaxDays'])">
|
||||
<template #label>{{ i18n.ts._role._options.scheduleNoteMaxDays }}</template>
|
||||
<template #suffix>{{ policies.scheduleNoteMaxDays }}</template>
|
||||
<MkInput v-model="policies.scheduleNoteMaxDays" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
@ -5078,6 +5078,8 @@ export type components = {
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canScheduleNote: boolean;
|
||||
scheduleNoteLimit: number;
|
||||
scheduleNoteMaxDays: number;
|
||||
canInitiateConversation: boolean;
|
||||
canCreateContent: boolean;
|
||||
canUpdateContent: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user