diff --git a/locales/en-US.yml b/locales/en-US.yml index 3ec46cc61b00..2117fb056eed 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1365,8 +1365,8 @@ _abuseReportCategory: otherBreach_description: "Other actions that violate the terms but do not fall under specific categories" violationRights: "Rights Infringement or Impersonation (Reported by Rights Holder)" violationRights_description: "Posts infringing the rights (such as copyright or trademark) of the rights holder or impersonation" - violationRightsOther: "Rights Infringement or Impersonation (Reported by Third Party)" - violationRightsOther_description: "Posts infringing the rights (such as copyright or trademark) of others or impersonation\nIf reported by a third party, cases outside the scope of non-complaint offenses as defined by law may not be addressed" + violationRightsOther: "Rights Infringement (Reported by Third Party)" + violationRightsOther_description: "Posts infringing the rights (such as copyright or trademark) of others\nIf reported by a third party, cases outside the scope of non-complaint offenses as defined by law may not be addressed" notLike: "Dislike This Person" notLike_description: "Users or posts that you find unpleasant for personal reasons" other: "Other" @@ -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,8 @@ _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" username: "Username" @@ -2348,7 +2352,7 @@ _profile: sectionName: "Section name" sectionNameNoneDescription: "Do not display the section name" sectionNameNone: "Section without name" - policyDisplayLimitExceeded: "The number of items displayed exceeds the current support plan's limit ({max}). This item will not be displayed. You can upgrade your plan [here](https://go.misskey.io/donate)." + policyDisplayLimitExceeded: "The number of items displayed exceeds the current support plan's limit ({max}). This item will not be displayed.\nYou can upgrade your plan [here](https://go.misskey.io/donate)." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" diff --git a/locales/index.d.ts b/locales/index.d.ts index 707e058e8e96..dce1d15b8015 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5490,11 +5490,11 @@ export interface Locale extends ILocale { */ "violationRights_description": string; /** - * 権利侵害やなりすまし(第三者による通報) + * 権利侵害(第三者による通報) */ "violationRightsOther": string; /** - * 他人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為 + * 他人の著作権、商標権、またはその他の権利を侵害する行為 * 第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります */ "violationRightsOther_description": string; @@ -7019,6 +7019,14 @@ export interface Locale extends ILocale { * 予約投稿の許可 */ "canScheduleNote": string; + /** + * 予約投稿の最大数 + */ + "scheduleNoteLimit": string; + /** + * 予約投稿の最大日数 + */ + "scheduleNoteMaxDays": string; /** * メンション、リプライ、引用の許可 */ @@ -9068,9 +9076,14 @@ export interface Locale extends ILocale { "f": string; }; /** - * [NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。 + * 現在の支援プランで予約できる日数の上限は{max}日です。 + * [ここ](https://go.misskey.io/donate)からプランをアップグレードできます。 + */ + "policyScheduleNoteMaxDaysExceeded": ParameterizedString<"max">; + /** + * 投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。 */ - "guidelineInfo": ParameterizedString<"nsfwGuideUrl">; + "tosAndGuidelinesInfo": ParameterizedString<"tosUrl">; }; "_profile": { /** @@ -9166,7 +9179,8 @@ export interface Locale extends ILocale { */ "sectionNameNone": string; /** - * 現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。 + * 現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。 + * [ここ](https://go.misskey.io/donate)からプランをアップグレードできます。 */ "policyDisplayLimitExceeded": ParameterizedString<"max">; }; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5612736be418..e5038ead054b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1370,8 +1370,8 @@ _abuseReportCategory: otherBreach_description: "明確に分類されないその他の規約違反行為" violationRights: "権利侵害やなりすまし(侵害を受けた権利者本人によるご申告)" violationRights_description: "権利者本人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為" - violationRightsOther: "権利侵害やなりすまし(第三者による通報)" - violationRightsOther_description: "他人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為\n第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります" + violationRightsOther: "権利侵害(第三者による通報)" + violationRightsOther_description: "他人の著作権、商標権、またはその他の権利を侵害する行為\n第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります" notLike: "この人が気に入らない" notLike_description: "個人的な理由で不快と感じるユーザーや投稿" other: "その他" @@ -1808,6 +1808,8 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canScheduleNote: "予約投稿の許可" + scheduleNoteLimit: "予約投稿の最大数" + scheduleNoteMaxDays: "予約投稿の最大日数" canInitiateConversation: "メンション、リプライ、引用の許可" canCreateContent: "コンテンツの作成" canUpdateContent: "コンテンツの編集" @@ -2377,7 +2379,8 @@ _postForm: d: "言いたいことは?" e: "ここに書いてください" f: "あなたが書くのを待っています..." - guidelineInfo: "[NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。" + policyScheduleNoteMaxDaysExceeded: "現在の支援プランで予約できる日数の上限は{max}日です。\n[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。" + tosAndGuidelinesInfo: "投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。" _profile: name: "名前" @@ -2403,7 +2406,7 @@ _profile: sectionName: "セクション名" sectionNameNoneDescription: "セクション名を表示しないようにする" sectionNameNone: "名前が表示されないセクション" - policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。" + policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。\n[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。" _exportOrImport: allNotes: "全てのノート" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d90bd9adfa11..6bc34827ae40 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1362,8 +1362,8 @@ _abuseReportCategory: otherBreach_description: "명확하게 분류되지 않는 기타 규약 위반 행위" violationRights: "권리 침해 또는 사칭 (권리자 본인에 의한 신고)" violationRights_description: "권리자 본인의 저작권, 상표권 또는 기타 권리를 침해하는 게시물 및 사칭 행위" - violationRightsOther: "권리 침해 또는 사칭 (제3자에 의한 신고)" - violationRightsOther_description: "타인의 저작권, 상표권 또는 기타 권리를 침해하는 게시물 및 사칭 행위\n제3자에 의한 신고의 경우, 법으로 정해진 비친고죄 범위 외의 사례에는 대응할 수 없습니다" + violationRightsOther: "권리 침해 (제3자에 의한 신고)" + violationRightsOther_description: "타인의 저작권, 상표권 또는 기타 권리를 침해하는 행위\n제3자에 의한 신고의 경우, 법으로 정해진 비친고죄 범위 외의 사례에는 대응할 수 없습니다" notLike: "이 사람이 마음에 들지 않음" notLike_description: "개인적인 이유로 불쾌감을 느끼는 사용자나 게시물" other: "기타" @@ -1791,6 +1791,8 @@ _role: ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" canScheduleNote: "노트 예약 허용" + scheduleNoteLimit: "노트 예약 한도" + scheduleNoteMaxDays: "노트 예약 최대 일수" mentionMax: "노트에 넣을 수 있는 멘션 수" canCreateContent: "컨텐츠 생성 허용" canUpdateContent: "컨텐츠 수정 허용" @@ -2310,6 +2312,8 @@ _postForm: d: "말하고 싶은 게 있나요?" e: "여기에 적어 주세요" f: "글 쓰기를 기다려요…" + policyScheduleNoteMaxDaysExceeded: "현재 지원 플랜의 예약 가능한 최대 일수는 {max}일입니다.\n[여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다." + tosAndGuidelinesInfo: "노트를 게시하기 전에 [이용약관]({tosUrl})과 [NSFW 가이드라인](https://go.misskey.io/media-guideline)을 반드시 읽어 주세요." _profile: name: "이름" username: "사용자 이름" @@ -2334,7 +2338,7 @@ _profile: sectionName: "섹션 이름" sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다" sectionNameNone: "이름이 표시되지 않는 섹션" - policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다. [여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다." + policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다.\n[여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다." _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" diff --git a/package.json b/package.json index 5fddc5249a99..9cccb37517fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.5.0-io.7b", + "version": "2024.5.0-io.7c", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 71e430c2d4a9..8a43e100d9f9 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -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!); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 1b8091cc24c7..60c935887fc4 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -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)), diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 6cbea17089ca..69b68c41499c 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -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, diff --git a/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts index 8c441a9cc4a7..c55eaa40fe1a 100644 --- a/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts @@ -37,7 +37,7 @@ export class CheckMissingScheduledNoteProcessorService { } const query = this.scheduledNotesRepository.createQueryBuilder('draft') - .where('draft.scheduledAt < now() - interval \'5 minutes\'').orderBy('draft.createdAt', 'ASC'); + .where('draft.scheduledAt < now() + interval \'10 minutes\'').orderBy('draft.createdAt', 'ASC'); let lastId = '0'; while (true) { diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index a56a2d85b6e8..5f264bb73387 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -97,10 +97,15 @@ export class SigninApiService { reply.code(429); return { error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, + info: { + message: 'Too many failed attempts to sign in.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '6c181469-ecb9-42d2-82c9-60db5486a819', + }, + } }; } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index c90c9daf9675..ddccbdae5f9c 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -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 { // 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 { // 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; diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index ff05359b5939..f47a644de527 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -51,6 +51,10 @@ class AntennaChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -59,7 +63,6 @@ class AntennaChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } else { diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 96ada8c97df1..7ebc2ed56fa4 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -57,6 +57,10 @@ class ChannelChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -65,7 +69,6 @@ class ChannelChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 5816187401f4..371449cfdaf9 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -87,6 +87,10 @@ class GlobalTimelineChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -95,7 +99,6 @@ class GlobalTimelineChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index c7384fdf96e0..0dbd70b99f01 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -60,7 +60,9 @@ class HashtagChannel extends Channel { } } - this.connection.cacheNote(note); + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 34890854c3de..7b38cb59c3fa 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -91,6 +91,10 @@ class HomeTimelineChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -99,7 +103,6 @@ class HomeTimelineChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 47d534aa1a93..4103fb5dc499 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -105,6 +105,10 @@ class HybridTimelineChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -113,7 +117,6 @@ class HybridTimelineChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index e2192cfad221..99d94ace9ad0 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -90,6 +90,10 @@ class LocalTimelineChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -98,7 +102,6 @@ class LocalTimelineChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index f93f625681bf..b7f840c6c3d0 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -38,7 +38,11 @@ class MainChannel extends Channel { const note = await this.noteEntityService.pack(data.body.note.id, this.user, { detail: true, }); - this.connection.cacheNote(note); + + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + data.body.note = note; } break; @@ -51,7 +55,11 @@ class MainChannel extends Channel { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true, }); - this.connection.cacheNote(note); + + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + data.body = note; } break; diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 67e5cd138f3e..b735507e24a5 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -73,6 +73,10 @@ class RoleTimelineChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -81,7 +85,6 @@ class RoleTimelineChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } else { diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index d9407ac5dd59..4b8df4368f6d 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -130,6 +130,10 @@ class UserListChannel extends Channel { } } + if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) { + this.connection.cacheNote(note); + } + if (this.minimize && ['public', 'home'].includes(note.visibility)) { this.send('note', { id: note.id, myReaction: note.myReaction, @@ -138,7 +142,6 @@ class UserListChannel extends Channel { renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, }); } else { - this.connection.cacheNote(note); this.send('note', note); } } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index ddf4a157def0..baca7f9f5c8d 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -82,11 +82,23 @@ SPDX-License-Identifier: AGPL-3.0-only
-
- {{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }} +
+
+ + + {{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }} + +
+
+ + +
+
- + + + @@ -217,6 +229,9 @@ if (props.initialVisibleUsers) { } const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); const scheduledTime = ref(null); +const scheduledTimeExceededPolicy = computed(() => + scheduledTime.value ? (scheduledTime.value.getTime() - Date.now()) / 86_400_000 > $i!.policies.scheduleNoteMaxDays : false +); const autocompleteTextareaInput = ref(null); const autocompleteCwInput = ref(null); const autocompleteHashtagsInput = ref(null); @@ -228,8 +243,6 @@ const imeText = ref(''); const showingOptions = ref(false); const textAreaReadOnly = ref(false); -const nsfwGuideUrl = 'https://go.misskey.io/media-guideline'; - const draftKey = computed((): string => { let key = channel.value ? `channel:${channel.value.id}` : ''; @@ -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')); @@ -597,6 +614,7 @@ function removeVisibleUser(user) { async function setScheduledTime() { const { canceled, result: date } = await os.inputDateTime({ title: i18n.ts.setScheduledTime, + default: scheduledTime.value ?? undefined, }); if (canceled) return; @@ -1392,8 +1410,10 @@ defineExpose({ .scheduledTime { display: flex; - padding: 8px 24px; + padding: 8px 12px; gap: 4px; + align-items: center; + font-size: 90%; background: var(--infoBg); } @@ -1443,10 +1463,6 @@ defineExpose({ color: var(--accent); } -.guidelineInfo { - margin-top: 8px; -} - @container (max-width: 500px) { .headerRight { font-size: .9em; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 1483b9d0c094..e84958a69be8 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -79,6 +79,8 @@ export const ROLE_POLICIES = [ 'ltlAvailable', 'canPublicNote', 'canScheduleNote', + 'scheduleNoteLimit', + 'scheduleNoteMaxDays', 'canInitiateConversation', 'canCreateContent', 'canUpdateContent', diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 9a5243926808..5a1071a926cf 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -79,7 +79,7 @@ export async function apiErrorHandler(err: Misskey.api.APIError, endpoint?: stri } 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_')) { title = i18n.ts.youCannotCreateAnymore; text = `${i18n.ts.error}: ${err.id}`; } diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index d4d5b50500ca..f40d1b742472 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -185,6 +185,44 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + +
+
+ + + + +
+ + + + + + + + +
+
+