Compare commits

...

16 Commits

Author SHA1 Message Date
syuilo a6bd5a1903
Merge b97f4a9407 into 218070eb13 2025-09-25 10:28:19 +00:00
syuilo b97f4a9407 Update MkPostForm.vue 2025-09-25 19:28:15 +09:00
syuilo f0c297c483 wip 2025-09-25 17:56:04 +09:00
syuilo 9bd4dbc906 Update MkPostForm.vue 2025-09-25 17:55:16 +09:00
syuilo 4ac1dc8740 wip 2025-09-25 17:53:06 +09:00
syuilo 6471863ba5 wip 2025-09-25 17:36:33 +09:00
syuilo 45074de293 Update NoteDraftService.ts 2025-09-25 17:33:01 +09:00
syuilo 5e766ae230 wip 2025-09-25 17:28:28 +09:00
syuilo 5d2bc61ede Update NoteDraftService.ts 2025-09-25 16:16:30 +09:00
syuilo 1fe2264f41 Update NoteCreateService.ts 2025-09-25 16:08:48 +09:00
syuilo 2b24dd6075 Update NoteDraftEntityService.ts 2025-09-25 16:02:12 +09:00
syuilo 43f82593ab wip 2025-09-25 16:01:27 +09:00
syuilo 41336d880f wip 2025-09-25 15:56:13 +09:00
syuilo 2faa39f5ae wip 2025-09-25 15:47:13 +09:00
syuilo 1df36d5fc7 Update NoteCreateService.ts 2025-09-25 15:37:57 +09:00
syuilo 914eb0bd35 wip 2025-09-25 15:32:50 +09:00
20 changed files with 183 additions and 95 deletions

View File

@ -5,6 +5,7 @@
### General
- Feat: 予約投稿ができるようになりました
- デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
### Client

4
locales/index.d.ts vendored
View File

@ -7957,6 +7957,10 @@ export interface Locale extends ILocale {
*
*/
"noteDraftLimit": string;
/**
* 稿
*/
"scheduledNoteLimit": string;
/**
* 使
*/

View File

@ -2062,6 +2062,7 @@ _role:
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
scheduledNoteLimit: "予約投稿の同時作成可能数"
watermarkAvailable: "ウォーターマーク機能の使用可否"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"

View File

@ -236,24 +236,25 @@ export class NoteCreateService implements OnApplicationShutdown {
isBot: MiUser['isBot'];
isCat: MiUser['isCat'];
}, data: {
createdAt: Date;
replyId: MiNote['id'] | null;
renoteId: MiNote['id'] | null;
fileIds: MiDriveFile['id'][];
text: string | null;
cw: string | null;
visibility: string | null;
visibility: string;
visibleUserIds: MiUser['id'][];
channelId: MiChannel['id'] | null;
localOnly: boolean | null;
localOnly: boolean;
reactionAcceptance: MiNote['reactionAcceptance'];
poll: IPoll | null;
apMentions?: MinimumUser[] | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
}): Promise<MiNote> {
let visibleUsers: MiUser[] = [];
if (data.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
}
const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({
id: In(data.visibleUserIds),
}) : [];
let files: MiDriveFile[] = [];
if (data.fileIds.length > 0) {
@ -352,13 +353,11 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
if (ps.poll) {
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now()) {
if (data.poll) {
if (data.poll.expiresAt != null) {
if (data.poll.expiresAt.getTime() < Date.now()) {
throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time');
}
} else if (typeof ps.poll.expiredAfter === 'number') {
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
}
}
@ -372,14 +371,10 @@ export class NoteCreateService implements OnApplicationShutdown {
}
return this.create(user, {
createdAt: new Date(),
createdAt: data.createdAt,
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: data.text ?? undefined,
poll: data.poll,
text: data.text,
reply,
renote,
cw: data.cw,
@ -388,9 +383,9 @@ export class NoteCreateService implements OnApplicationShutdown {
visibility: data.visibility,
visibleUsers,
channel,
apMentions: data.noExtractMentions ? [] : undefined,
apHashtags: data.noExtractHashtags ? [] : undefined,
apEmojis: data.noExtractEmojis ? [] : undefined,
apMentions: data.apMentions,
apHashtags: data.apHashtags,
apEmojis: data.apEmojis,
});
}

View File

@ -5,14 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { DI } from '@/di-symbols.js';
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { IPoll } from '@/models/Poll.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -61,12 +59,24 @@ export class NoteDraftService {
@bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
//#region check draft limit
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id,
});
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
if (currentCount >= policies.noteDraftLimit) {
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
}
if (data.isActuallyScheduled) {
const currentScheduledCount = await this.noteDraftsRepository.countBy({
userId: me.id,
isActuallyScheduled: true,
});
if (currentScheduledCount >= policies.scheduledNoteLimit) {
throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes');
}
}
//#endregion
await this.validate(me, data);
@ -95,6 +105,20 @@ export class NoteDraftService {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
//#region check draft limit
const policies = await this.roleService.getUserPolicies(me.id);
if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
const currentScheduledCount = await this.noteDraftsRepository.countBy({
userId: me.id,
isActuallyScheduled: true,
});
if (currentScheduledCount >= policies.scheduledNoteLimit) {
throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes');
}
}
//#endregion
await this.validate(me, data);
const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update()

View File

@ -69,6 +69,7 @@ export type RolePolicies = {
chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
noteDraftLimit: number;
scheduledNoteLimit: number;
watermarkAvailable: boolean;
};
@ -116,6 +117,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
'audio/*',
],
noteDraftLimit: 10,
scheduledNoteLimit: 1,
watermarkAvailable: true,
};
@ -440,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return [...set];
}),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
};
}

View File

@ -129,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit {
allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId,
} : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : null,
...(opts.detail ? {
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
@ -140,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit {
detail: true,
skipHide: opts.skipHide,
})) : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : undefined,
} : {} ),
});

View File

@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
},
cw: {
type: 'string',
optional: true, nullable: true,
optional: false, nullable: true,
},
userId: {
type: 'string',
@ -37,27 +37,23 @@ export const packedNoteDraftSchema = {
},
replyId: {
type: 'string',
optional: true, nullable: true,
optional: false, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
renoteId: {
type: 'string',
optional: true, nullable: true,
optional: false, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
reply: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.',
},
renote: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.',
},
visibility: {
type: 'string',
@ -66,7 +62,7 @@ export const packedNoteDraftSchema = {
},
visibleUserIds: {
type: 'array',
optional: true, nullable: false,
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@ -75,7 +71,7 @@ export const packedNoteDraftSchema = {
},
fileIds: {
type: 'array',
optional: true, nullable: false,
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@ -93,11 +89,11 @@ export const packedNoteDraftSchema = {
},
hashtag: {
type: 'string',
optional: true, nullable: false,
optional: false, nullable: true,
},
poll: {
type: 'object',
optional: true, nullable: true,
optional: false, nullable: true,
properties: {
expiresAt: {
type: 'string',
@ -124,9 +120,8 @@ export const packedNoteDraftSchema = {
},
channelId: {
type: 'string',
optional: true, nullable: true,
optional: false, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
channel: {
type: 'object',
@ -160,7 +155,7 @@ export const packedNoteDraftSchema = {
},
localOnly: {
type: 'boolean',
optional: true, nullable: false,
optional: false, nullable: false,
},
reactionAcceptance: {
type: 'string',

View File

@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
scheduledNoteLimit: {
type: 'integer',
optional: false, nullable: false,
},
watermarkAvailable: {
type: 'boolean',
optional: false, nullable: false,

View File

@ -40,11 +40,11 @@ export class PostScheduledNoteProcessorService {
const note = await this.noteCreateService.fetchAndCreate(draft.user, {
createdAt: new Date(),
fileIds: draft.fileIds,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
poll: draft.hasPoll ? {
choices: draft.pollChoices,
multiple: draft.pollMultiple,
expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null,
} : null,
text: draft.text ?? null,
replyId: draft.replyId,
renoteId: draft.renoteId,
@ -54,9 +54,6 @@ export class PostScheduledNoteProcessorService {
visibility: draft.visibility,
visibleUserIds: draft.visibleUserIds,
channelId: draft.channelId,
apMentions: draft.noExtractMentions ? [] : undefined,
apHashtags: draft.noExtractHashtags ? [] : undefined,
apEmojis: draft.noExtractEmojis ? [] : undefined,
});
// await不要

View File

@ -227,8 +227,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : null,
text: ps.text ?? null,
replyId: ps.replyId ?? null,
renoteId: ps.renoteId ?? null,
@ -246,16 +246,46 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
createdNote: await this.noteEntityService.pack(note, me),
};
} catch (e) {
} catch (err) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
if (err instanceof IdentifiableError) {
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
} else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
} else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') {
throw new ApiError(meta.errors.noSuchFile);
} else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') {
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') {
throw new ApiError(meta.errors.cannotReRenote);
} else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') {
throw new ApiError(meta.errors.youHaveBeenBlocked);
} else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') {
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') {
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') {
throw new ApiError(meta.errors.noSuchChannel);
} else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') {
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
} else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') {
throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
} else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') {
throw new ApiError(meta.errors.youHaveBeenBlocked);
} else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
} else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') {
throw new ApiError(meta.errors.noSuchChannel);
}
}
throw e;
throw err;
}
});
}

View File

@ -124,6 +124,12 @@ export const meta = {
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
},
tooManyScheduledNotes: {
message: 'You cannot create scheduled notes any more.',
code: 'TOO_MANY_SCHEDULED_NOTES',
id: '22ae69eb-09e3-4541-a850-773cfa45e693',
},
cannotRenoteToExternal: {
message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL',
@ -244,6 +250,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}

View File

@ -159,6 +159,12 @@ export const meta = {
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
},
tooManyScheduledNotes: {
message: 'You cannot create scheduled notes any more.',
code: 'TOO_MANY_SCHEDULED_NOTES',
id: '02f5df79-08ae-4a33-8524-f1503c8f6212',
},
},
limit: {
@ -287,6 +293,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
case '4de0363a-3046-481b-9b0f-feff3e211025':
throw new ApiError(meta.errors.containsTooManyMentions);
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}

View File

@ -614,13 +614,13 @@ function showOtherSettings() {
action: () => {
toggleReactionAcceptance();
},
}, {
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.schedulePost + '...',
action: () => {
schedule();
},
}, { type: 'divider' }, {
}] : []), { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
@ -1181,8 +1181,10 @@ function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent)
}
function showDraftMenu(ev: MouseEvent) {
function showDraftsDialog() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {}, {
function showDraftsDialog(scheduled: boolean) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
scheduled,
}, {
restore: async (draft: Misskey.entities.NoteDraft) => {
text.value = draft.text ?? '';
useCw.value = draft.cw != null;
@ -1254,14 +1256,14 @@ function showDraftMenu(ev: MouseEvent) {
text: i18n.ts._drafts.listDrafts,
icon: 'ti ti-cloud-download',
action: () => {
showDraftsDialog();
showDraftsDialog(false);
},
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog();
showDraftsDialog(true);
},
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}

View File

@ -76,7 +76,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints>(
} else if (err.code === 'ROLE_PERMISSION_DENIED') {
title = i18n.ts.permissionDeniedError;
text = i18n.ts.permissionDeniedErrorDescription;
} else if (err.code.startsWith('TOO_MANY')) {
} else if (err.code.startsWith('TOO_MANY')) { // TODO: バックエンドに kind: client/contentsLimitExceeded みたいな感じで送るように統一してもらってそれで判定する
title = i18n.ts.youCannotCreateAnymore;
text = `${i18n.ts.error}: ${err.id}`;
} else if (err.message.startsWith('Unexpected token')) {

View File

@ -802,6 +802,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])">
<template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template>
<template #suffix>
<span v-if="role.policies.scheduledNoteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.scheduledNoteLimit.value }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduledNoteLimit)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.scheduledNoteLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.scheduledNoteLimit.value" :disabled="role.policies.scheduledNoteLimit.useDefault" type="number" :readonly="readonly">
</MkInput>
<MkRange v-model="role.policies.scheduledNoteLimit.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.watermarkAvailable, 'watermarkAvailable'])">
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
<template #suffix>
@ -831,6 +850,7 @@ import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import * as Misskey from 'misskey-js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@ -842,7 +862,6 @@ import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/utility/clone.js';
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
const emit = defineEmits<{
(ev: 'update:modelValue', v: any): void;

View File

@ -304,6 +304,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])">
<template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template>
<template #suffix>{{ policies.scheduledNoteLimit }}</template>
<MkInput v-model="policies.scheduledNoteLimit" type="number" :min="0">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])">
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
<template #suffix>{{ policies.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}</template>

View File

@ -3441,7 +3441,7 @@ type RoleLite = components['schemas']['RoleLite'];
type RolePolicies = components['schemas']['RolePolicies'];
// @public (undocumented)
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "watermarkAvailable"];
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
// @public (undocumented)
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];

View File

@ -4424,42 +4424,31 @@ export type components = {
/** Format: date-time */
createdAt: string;
text: string | null;
cw?: string | null;
cw: string | null;
/** Format: id */
userId: string;
user: components['schemas']['UserLite'];
/**
* Format: id
* @example xxxxxxxxxx
*/
replyId?: string | null;
/**
* Format: id
* @example xxxxxxxxxx
*/
renoteId?: string | null;
/** @description The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null. */
/** Format: id */
replyId: string | null;
/** Format: id */
renoteId: string | null;
reply?: components['schemas']['Note'] | null;
/** @description The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null. */
renote?: components['schemas']['Note'] | null;
/** @enum {string} */
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[];
fileIds?: string[];
visibleUserIds: string[];
fileIds: string[];
files?: components['schemas']['DriveFile'][];
hashtag?: string;
poll?: {
hashtag: string | null;
poll: {
/** Format: date-time */
expiresAt?: string | null;
expiredAfter?: number | null;
multiple: boolean;
choices: string[];
} | null;
/**
* Format: id
* @example xxxxxxxxxx
*/
channelId?: string | null;
/** Format: id */
channelId: string | null;
channel?: {
id: string;
name: string;
@ -4468,7 +4457,7 @@ export type components = {
allowRenoteToExternal: boolean;
userId: string | null;
} | null;
localOnly?: boolean;
localOnly: boolean;
/** @enum {string|null} */
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
scheduledAt: number | null;
@ -5289,6 +5278,7 @@ export type components = {
/** @enum {string} */
chatAvailability: 'available' | 'readonly' | 'unavailable';
noteDraftLimit: number;
scheduledNoteLimit: number;
watermarkAvailable: boolean;
};
ReversiGameLite: {

View File

@ -229,6 +229,7 @@ export const rolePolicies = [
'chatAvailability',
'uploadableFileTypes',
'noteDraftLimit',
'scheduledNoteLimit',
'watermarkAvailable',
] as const;