%qEf@Ov(`XrIpOlOi3FgQ(`F$I)usw5ZX%W8p!h5e|i(zo4p19y(
zEl4WLI%KLKcxS*3ntRt}KgV4yv^wAW{vmiAU-Ad4h9f);E$)LWcg&;o
z>GBW6jD>;$>!)53XxCtfWvb~cRG*$O%=widPV*zMt_)LKU>zmxy_siw3-&dH{2C3h
z3@ta{#L)~X>*rIR9xvN;fzz4@Kq)o&J+)7d9Ao1KMqEK-1&3To{8w`?rUaZxqDe
zm*5>01?lpu!mdK|%$w}x@qou*@;63gEP-V)HuVQ*x9?|UIMa1b>o5uJlJ-pJs%|D~
zu6h8&n-bN{AUQB||IvO2w9Fw?8RC=F;mK0A)Ve!LNtylxmX4EJKSo|(tuqglGzyAg
zG|`Z<=1YwIb;>7R`%S
z3eN9|dJrU6{>gMcc6ogAi#K6ALTa*LFXhJgNykI!pU)XR9l+VXLFjc@w|i*+XHC>23$0|ob-e146Qz0f
zS58{U;)rI~>Jd5iy%E@P*4vokw8qW6B+)pk$G}x}LL#t7hzE{0o=>RQu-a+TCrRS$
zR8|~ll6uHidK=sk>xBRNy{*Gg7OBGa8oYhCgnn$lbr))zg1O9&Gwc>>+QxM$D5`@N
zH8BT*G0p8yuv)E0&mAo9!W9}5&F;8WtlTb?c3+)fqu#V^3U^*1#L?=*0%A;pr~jI?
z6|C^oB~9$?9DLX4Abpo)`tg1#HMRSf_Cf}>4|+E{G^dWf*0kvJgzyrXvN_#1$+Hi-
zluUeCZ9?Q`7($r3JMlaEV~b(PAZ5cR=Cb%KDNoOfj@_{?(qxa`fi&FX0~_xhsNj1r
ztUXI-j4x3rrNYcp3d$O~>cWe*FCQAm_k<@|wztRz=Sv43G1Mq0ZsbTFhwEGa9y8`0
zq!{?rcE5}6-$LqGMI;qb)WuchBeTOQrUf`QFU(w_SF5)|
k0gvB5yx_L_`&hltOsT!`;l_y#or`}Nl^p0_>C0gM1EQ^7b^rhX
literal 0
HcmV?d00001
diff --git a/packages/backend/assets/tabler-badges/calendar-exclamation.png b/packages/backend/assets/tabler-badges/calendar-exclamation.png
new file mode 100644
index 0000000000000000000000000000000000000000..7944fac88f3ede3e5995dcb3c1141feda536e5f8
GIT binary patch
literal 1933
zcmYk7c~sKr8pnS>7Dd2aM-el%GSeckT&5KFg>(wajhncanoE{op2kFtFuRndWr`+n
zW=31wGc__n%e85A^pu8UVw09uN_JHemy6Y$d!KWj_xpLj=Xt;9`R~moAM{p78X*Ay
zP$&6#99C-hMk7>|=j-A#4oXo=_MxT#07`eGL4aFks5Bdo1(S}GNLBzsc~Jp$Kqvqz
zX`_um_*V^7GWI)%D%tY;3`EJgkpIoc<-oqqA6NPt<)iG4oPSigO1>%pQ#yDX0v^2j
zjX`As!2j+O@OY5_EpdGv03iHtiQ&pO(EnrQ6}Vj5^s91XCdtFqKdtN|?OC3lryldE
z7OSWGL+Du0E3!lwjBQxiw5&YHR=H?}o7$F%qCCutTFa$cKbR_7b-
z%<(`xtyvhHOBCI%u9+^Mo(ZDSL~m}C+^jASs?AvVA{45aZuweK`N5_d&)bYNwC=7Q
z9BZ>0pwuwDb6t203PQ3N(5_Lwr!`JqoK{FIj62*|i=18=XFp``e?nW{8EqCj8l<|P
z0Y&EUFl}3jzw{>s#~o>?Mb4(=VQSo{iqz_*yVtxz3$xEL>h^3$+w+Bho#o|L3%J56
zSnG|26MGttuV_@Qnk&vp9GQdThg2h&o2}xd7XAie3)V^So}_ku7c#uU>qSfsIuDC8
z!nyw0jKe0mGQb21H;Vo&(zmR8?bN(yBurb)%)oBWD3lOFXoYt2n{XF@3fY&Gk~3JP
z`g-SLQ11=>`H!3CZ7*HG4lfU}hta&pYm)28mz8hdypNk-l^Rdl8??xrZPF2*Q^^Nc
zJCd<|KQ3PB+;@c?eMIr0+wRL^!JyOD@nclFik7THbu4eaYXh{a>KLnu4rS8*<HK7TiPDa;AlM_ytU
zT?j4}z!K->b^ec;0U$noe^U(!{D4|!6TPf~uERy@VZZ%e?!$_jkHf6gZ?y%Zz3>%n
zC6>*O8D}gq@2Y9sY(Qz?lprY~l@LKY(>lNbl{_|x-S=Q;lX%k=9+sNv1M8vXhh<7>
zWE0jZgl>b-JZCf`DKf7Vv_Iqva8G1y?eED659?QBD6<%o`o1cCw3A+zG;QDvSw17I
zT&cd^WQ%q(+zvfGg3QQim`eF5@?9^Sd~U=={6DE%2QAcgfp5I-
z@X*8Irz2SJHA0NX>{i4=iyg5D^8M>|C-g6+eA29rA7yu?-HS04n28z1A@UYT^h&0@
zcGt_)NzVn&gBrLr{?uJL_i<=|HR4lx)?#3vQ`AL)8vjvAnkIqaN(3e8U4B3BUxh`Q
zu-s8votPAYpZf!p>={d<|I20zHsu5HfoAroa&PE|UB^#DAn8l34@Lc5xa2
z4LZiC2jVbrEV-%~RX?!*Zy{3C+gjX6l;57f9Kc$+jdZFP#GXHuhJLxpCX|sR3A*+}
z-YoLI(iGP5GNPgh(NHv^+I&LibJqDOCri5*<>t(p
zi~Niu4`DPwe#<6pYG<3QxM`VZJygSx$6E{
z5J9#8`lap&So0c!6I!f0vWkuZyvkZy%TGcxW6kt_x~DB-G+Z0!Mp%gMu2^N|+R^&y
z51N|1?bR7sd)BxU_EO%a&JJNeJs=TX2nlT-yyLQX)xm+8DBsh<>^e~Dp#Dg6h&Npz
zKn%a|$q=brU0M7!-lf3#%@4Zo9f!8T#YEA8%xB@=<0erVZ?}
zPV`4;v*Q2F5MMZJV&Oh$qj&Drfzm&1TEcf5u0cEnCkGU$KrY86zmT)7*paivAjA8j
jl^!8_FUH+wIddKCd$sdsE2`UR<4YoW9`vYj3(x*9G&Vb$
literal 0
HcmV?d00001
diff --git a/packages/backend/assets/tabler-badges/calendar-time.png b/packages/backend/assets/tabler-badges/calendar-time.png
new file mode 100644
index 0000000000000000000000000000000000000000..0443baee2614989c8571a40a24a5f50a00fc5879
GIT binary patch
literal 2801
zcmYjTc{tSFAN`JDm@%W&NDLDOkt{J8WoeR~?E5mb49%1!#Mmd4XhHVf$o|@+C|i~m
zNrOUUX&7smL<#Ym-uL}Izu$eHbIffE1GAa(_VOtQzQq9Qyxla$Gcu*=kjh
znA2LLcOPr@WrqEnnCtC5s)48
zbislk-+r>3F2@m^+Hs0=u4lu5ZEQG=sv4O2;x{s6vUDS?t*}3+9N*?cRZ@YYrQ108
zjD>SbVC%d$(YHzm!jdbfT88R<#~umwMuy8$Gtg$2#>|&*M$Oo@i!oin!bPh|mGab#
z#fIGTShevflc>isWT$NeTD*kbg#NFEjGUnWT?7JdJnvjT6V^V?X
za)Ndh!_fz&FZqs^UBkb%t(yu;Tt1N4ATRC3+WG{UJVA+k8Ej{>UpZ&LgU8*k(^+&^
zD}Xua?xU#N`z?Jd`Gt7gs}anhVt%YyMVi*k;RBy;ON|o}U6u-nK;%BT+v!cmf?MC>
zOJ>wj#Mq*w&!Fg);t}K9mP&+^B9?2D#06;_zCae~#(6SS7uGf~`S&7Jfpi75HB91m-M
z-;AQaOhBEL%$c9zlsAfyBeYV-wGXWo2WzzwFOI1hWTsXrbJ^dZGRO;-yIyfy
zJh#zHkM9}tZ#GkR^r#9DNzd7nfQgCfW>gGxCmcUuT1ZKCp6yoK&6a$B=;-U+9HXoS
z4m51~Mj}0UYZCJQhLZWZug~N4Z@)vMUUB>i!Y02B=YUQBSU0DF!f|q9cn93l#4A
zz8#Mae7{rVJ9>38sHaiZ6$(~Dh8-`o*h)D8oa^U)BusOa{bONk2wN#CRE4MT7(cnI
z)H?i^HNigo3-iXE`zBp=&PFoA{bzcO#3N{sDdw1#oS6VnS99(7r7>aZu;muQRKVTC
znsZyHI2=|jCEyOcTHszZq1tV@Jl)IhiGybdUEO|eH0UV2DDSiTUQR1Ak+W&l!+-Rm2;+tH>yoz_!Pc1sv&`OnG&niv)TqGH
z?YR;Qe+BZh(7>Qjui8sCIBF%uOV1nE*fAv?HHKKB&Chg!e(g3jJBgFFt#^A77DVv;
zY!sM9P~B87Izq<0^M=?7HQVRw2ys^?cdy;*uU
z=(aKn=QmX|?nbXz9?%F8)Q!t@OX)YRWw7pz1*m?F
zw-u^#?9LP6WW-4rN$Ch7sKs-hFgYG{kLQNhPDW}?F
zo%I^uFh24Uu>kTLvMKAxa(9wcWlO`z9S}QZ}?d!GY1^BYeIg;*{2N1FB?T7_HXhI77QXIYzqn4hpkY
z1+#`|OOXgo5r3BxNl%~*v-;_0Lj88z*}gN}iXqYZ=T&6PKf&8EVxq~O-$Oo3IoP6q
zk9z=4vna_HO=GQ?jzD9dd%op~<|zoDCn@?7a}U;!uEiIy?lD^}*5)rt0{6iWEKBkf~d%^NY!lBDHYHMoy>H?CrH
zh~k#r@Q^#{vHHO)tLFa7C-CYy_oCd6m{Vl#Y#f;k5`%aV!~(Be?Dm7@YK|>wYmJt9
z$DF-s_;Y#ZTtCC6T-y(V1;2mj1T`NWM&`ycL<8%!F!>dEvR;f
zu+?=WtQehC{}rk$VpcEkl-^k#9^f4*!EzpsPQnIU_iE|!i-0}5;SvUq-%a>TpgOh>
z2e@2jlBC>hEOA+FmW}eesRjH8*Pa7bdy^qtD-J+qRlkL
zRZy%`BhDjzgD{bUV}#CmSTA*J>-q|>EZ^BHXWB-Yp>~Y=;R_&a8cgwgPPU%A
z0{kRig;2ltD``6~sH0?}+C^>^bt=94{FnuK$L)}NPlkxo@YRWJ{Z6&(YrT8l^R-1!`FDdngy_gjl&8gN8TQ-6t|T7hne-=PPW(i(bbCB0{P
zx#$rywjXqkcPWw;;{*2p&LdJG@ds+V5<`$u`J%fkik`hysvLQ!u<@xj>`>BRw9JBZ
z?AxjKec5~0GS)n=Y)E9c98Nn_A4!p)3yL$<9Uc_9?&4_lTltfNJE0y2X^Ot#Jf0
zcG4t8#xKT{trW|cq)cjxla7|{cy72R5hI@ {
+ }, data: NoteCreateOption, silent = false): Promise {
// チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@@ -425,18 +411,30 @@ export class NoteCreateService implements OnApplicationShutdown {
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', `Notes including mentions are limited to ${policies.mentionLimit} users.`);
}
- const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
+ if (!data.scheduledAt) {
+ const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
- setImmediate('post created', { signal: this.#shutdownController.signal }).then(
- () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
- () => { /* aborted, ignore this */ },
- );
+ setImmediate('post created', { signal: this.#shutdownController.signal }).then(
+ () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
+ () => { /* aborted, ignore this */ },
+ );
- return note;
+ return note;
+ } else {
+ if (!policies.canScheduleNote) {
+ throw new IdentifiableError('7cc42034-f7ab-4f7c-87b4-e00854479080', 'User has no permission to schedule notes.');
+ }
+
+ const draft = await this.insertScheduledNote(user, data);
+
+ await this.queueService.createScheduledNoteJob(draft.id, draft.scheduledAt!);
+
+ return draft;
+ }
}
@bindThis
- private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) {
+ private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: NoteCreateOption, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) {
const insert = new MiNote({
id: this.idService.gen(data.createdAt?.getTime()),
createdAt: data.createdAt!,
@@ -534,13 +532,40 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
+ @bindThis
+ private async insertScheduledNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: NoteCreateOption) {
+ const insert = new MiScheduledNote({
+ id: this.idService.gen(data.createdAt?.getTime()),
+ createdAt: data.createdAt!,
+ scheduledAt: data.scheduledAt!,
+ userId: user.id,
+ draft: data,
+ });
+
+ // 予約投稿を作成
+ try {
+ await this.scheduledNotesRepository.insert(insert);
+
+ return insert;
+ } catch (e) {
+ // duplicate key error
+ if (isDuplicateKeyValueError(e)) {
+ throw new IdentifiableError('5ea8e4f5-9d64-4e6c-92b8-9e2b5a4756bc', 'There is already a scheduled note with the same time.');
+ }
+
+ this.logger.error(`Failed to create scheduled note: ${e}`, { error: e });
+
+ throw e;
+ }
+ }
+
@bindThis
private async postNoteCreated(note: MiNote, user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
- }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
+ }, data: NoteCreateOption, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
const meta = await this.metaService.fetch();
this.notesChart.update(note, true);
@@ -792,12 +817,12 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private isRenote(note: Option): note is Option & { renote: MiNote } {
+ private isRenote(note: NoteCreateOption): note is NoteCreateOption & { renote: MiNote } {
return note.renote != null;
}
@bindThis
- private isQuote(note: Option): note is Option & { renote: MiNote } & (
+ private isQuote(note: NoteCreateOption): note is NoteCreateOption & { renote: MiNote } & (
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
) {
// NOTE: SYNC WITH misc/is-renote.ts
@@ -873,7 +898,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
+ private async renderNoteOrRenoteActivity(data: NoteCreateOption, note: MiNote) {
if (data.localOnly) return null;
const content = this.isRenote(data) && !this.isQuote(data)
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index a41c9c96e..fb5501c42 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
+import type { MiScheduledNote } from '@/models/ScheduledNote.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
import type { Config } from '@/config.js';
@@ -34,6 +35,11 @@ export class QueueService {
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {
+ this.ensureRepeatJobs();
+ }
+
+ @bindThis
+ private ensureRepeatJobs() {
this.systemQueue.add('tickCharts', {
}, {
repeat: { pattern: '55 * * * *' },
@@ -69,6 +75,12 @@ export class QueueService {
repeat: { pattern: '*/5 * * * *' },
removeOnComplete: true,
});
+
+ this.systemQueue.add('checkMissingScheduledNote', {
+ }, {
+ repeat: { pattern: '*/5 * * * *' },
+ removeOnComplete: true,
+ });
}
@bindThis
@@ -382,6 +394,18 @@ export class QueueService {
});
}
+ @bindThis
+ public createScheduledNoteJob(draftId: MiScheduledNote['id'], scheduledAt: Date) {
+ return this.systemQueue.add('scheduledNote', {
+ draftId,
+ }, {
+ jobId: `scheduledNote:${draftId}`,
+ delay: Math.max(scheduledAt.getTime() - Date.now(), 0) + Math.floor(Math.random() * 500 + 250),
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
@bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 1c205dca0..1b8091cc2 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -36,6 +36,7 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
+ canScheduleNote: boolean;
canInitiateConversation: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
@@ -77,6 +78,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
+ canScheduleNote: true,
canInitiateConversation: true,
canCreateContent: true,
canUpdateContent: true,
@@ -389,6 +391,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
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)),
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/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 1e6ff5a5a..9effcfac9 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -352,7 +352,7 @@ export class ApNoteService {
poll,
uri: note.id,
url: url,
- }, silent);
+ }, silent) as MiNote;
} catch (err: any) {
if (err.name !== 'duplicated') {
throw err;
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index a580ce44d..bd8f9a1cf 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -20,14 +20,16 @@ import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
+import type { ScheduledNoteEntityService } from './ScheduledNoteEntityService.js';
-const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
+const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]);
@Injectable()
export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private roleEntityService: RoleEntityService;
+ private scheduledNoteEntityService: ScheduledNoteEntityService;
constructor(
private moduleRef: ModuleRef,
@@ -52,6 +54,7 @@ export class NotificationEntityService implements OnModuleInit {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.roleEntityService = this.moduleRef.get('RoleEntityService');
+ this.scheduledNoteEntityService = this.moduleRef.get('ScheduledNoteEntityService');
}
/**
@@ -84,6 +87,11 @@ export class NotificationEntityService implements OnModuleInit {
// if the note has been deleted, don't show this notification
if (needsNote && !noteIfNeed) return null;
+ const needsDraft = 'draftId' in notification;
+ const draftIfNeed = needsDraft ? this.scheduledNoteEntityService.pack(notification.draftId, { id: meId }) : undefined;
+ // if the draft has been deleted, don't show this notification
+ if (needsDraft && !draftIfNeed) return null;
+
const needsUser = 'notifierId' in notification;
const userIfNeed = needsUser ? (
hint?.packedUsers != null
@@ -116,6 +124,7 @@ export class NotificationEntityService implements OnModuleInit {
createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
note: noteIfNeed,
+ draft: draftIfNeed,
reactions,
});
} else if (notification.type === 'renote:grouped') {
@@ -139,6 +148,7 @@ export class NotificationEntityService implements OnModuleInit {
createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
note: noteIfNeed,
+ draft: draftIfNeed,
users,
});
}
@@ -158,6 +168,7 @@ export class NotificationEntityService implements OnModuleInit {
userId: 'notifierId' in notification ? notification.notifierId : undefined,
...(userIfNeed != null ? { user: userIfNeed } : {}),
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
+ ...(draftIfNeed != null ? { draft: draftIfNeed } : {}),
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
diff --git a/packages/backend/src/core/entities/ScheduledNoteEntityService.ts b/packages/backend/src/core/entities/ScheduledNoteEntityService.ts
new file mode 100644
index 000000000..07f1c2bca
--- /dev/null
+++ b/packages/backend/src/core/entities/ScheduledNoteEntityService.ts
@@ -0,0 +1,76 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { ScheduledNotesRepository } from '@/models/_.js';
+import type { MiUser } from '@/models/User.js';
+import type { MiScheduledNote } from '@/models/ScheduledNote.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+import { DriveFileEntityService } from './DriveFileEntityService.js';
+
+@Injectable()
+export class ScheduledNoteEntityService {
+ constructor(
+ @Inject(DI.scheduledNotesRepository)
+ private scheduledNotesRepository: ScheduledNotesRepository,
+
+ private driveFileEntityService: DriveFileEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiScheduledNote['id'] | MiScheduledNote,
+ me: { id: MiUser['id'] },
+ ) : Promise> {
+ const item = typeof src === 'object' ? src : await this.scheduledNotesRepository.findOneByOrFail({ id: src, userId: me.id });
+
+ return {
+ id: item.id,
+ updatedAt: item.createdAt.toISOString(),
+ scheduledAt: item.scheduledAt?.toISOString() ?? null,
+ reason: item.reason ?? undefined,
+ channel: item.draft.channel ? {
+ id: item.draft.channel.id,
+ name: item.draft.channel.name,
+ } : undefined,
+ renote: item.draft.renote ? {
+ id: item.draft.renote.id,
+ text: (item.draft.renote.cw ?? item.draft.renote.text)?.substring(0, 100) ?? null,
+ user: {
+ id: item.draft.renote.userId,
+ username: item.draft.renote.user!.username,
+ host: item.draft.renote.user!.host,
+ },
+ } : undefined,
+ reply: item.draft.reply ? {
+ id: item.draft.reply.id,
+ text: (item.draft.reply.cw ?? item.draft.reply.text)?.substring(0, 100) ?? null,
+ user: {
+ id: item.draft.reply.userId,
+ username: item.draft.reply.user!.username,
+ host: item.draft.reply.user!.host,
+ },
+ } : undefined,
+ data: {
+ text: item.draft.text ?? null,
+ useCw: !!item.draft.cw,
+ cw: item.draft.cw ?? null,
+ visibility: item.draft.visibility as 'public' | 'followers' | 'home' | 'specified',
+ localOnly: item.draft.localOnly ?? false,
+ files: item.draft.files ? await this.driveFileEntityService.packMany(item.draft.files, me) : [],
+ poll: item.draft.poll ? { ...item.draft.poll, expiresAt: item.draft.poll.expiresAt?.getTime() ?? null, expiredAfter: null } : null,
+ visibleUserIds: item.draft.visibility === 'specified' ? item.draft.visibleUsers?.map(x => x.id) : undefined,
+ },
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ drafts: (MiScheduledNote['id'] | MiScheduledNote)[],
+ me: { id: MiUser['id'] },
+ ) : Promise[]> {
+ return (await Promise.allSettled(drafts.map(x => this.pack(x, me))))
+ .filter(result => result.status === 'fulfilled')
+ .map(result => (result as PromiseFulfilledResult>).value);
+ }
+}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index a70cce451..8b7b10b92 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -21,6 +21,7 @@ export const DI = {
announcementReadsRepository: Symbol('announcementReadsRepository'),
appsRepository: Symbol('appsRepository'),
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
+ scheduledNotesRepository: Symbol('scheduledNotesRepository'),
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'),
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 5f169c37d..10b144b16 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -31,6 +31,7 @@ import { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedNoteSchema } from '@/models/json-schema/note.js';
+import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
import { packedNotificationSchema } from '@/models/json-schema/notification.js';
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
@@ -77,6 +78,7 @@ export const refs = {
Announcement: packedAnnouncementSchema,
App: packedAppSchema,
Note: packedNoteSchema,
+ NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema,
NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema,
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index df88b9963..4747b51b5 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -5,6 +5,7 @@
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
+import { MiScheduledNote } from './ScheduledNote.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
@@ -77,6 +78,21 @@ export type MiNotification = {
id: string;
createdAt: string;
achievement: string;
+} | {
+ type: 'noteScheduled';
+ id: string;
+ createdAt: string;
+ draftId: MiScheduledNote['id'];
+} | {
+ type: 'scheduledNotePosted';
+ id: string;
+ createdAt: string;
+ noteId: MiNote['id'];
+} | {
+ type: 'scheduledNoteError';
+ id: string;
+ createdAt: string;
+ draftId: MiScheduledNote['id'];
} | {
type: 'app';
id: string;
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 83eee55ff..ced1ff812 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -39,6 +39,7 @@ import {
MiModerationLog,
MiMuting,
MiNote,
+ MiScheduledNote,
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
@@ -117,6 +118,12 @@ const $avatarDecorationsRepository: Provider = {
inject: [DI.db],
};
+const $scheduledNotesRepository: Provider = {
+ provide: DI.scheduledNotesRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiScheduledNote),
+ inject: [DI.db],
+};
+
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
@@ -517,6 +524,7 @@ const $abuseReportResolversRepository: Provider = {
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
+ $scheduledNotesRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
@@ -590,6 +598,7 @@ const $abuseReportResolversRepository: Provider = {
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
+ $scheduledNotesRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
diff --git a/packages/backend/src/models/ScheduledNote.ts b/packages/backend/src/models/ScheduledNote.ts
new file mode 100644
index 000000000..d573e2514
--- /dev/null
+++ b/packages/backend/src/models/ScheduledNote.ts
@@ -0,0 +1,54 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import type { NoteCreateOption } from '@/types.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+@Entity('note_scheduled')
+@Index(['userId', 'scheduledAt'], { unique: true })
+export class MiScheduledNote {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Note.',
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The scheduled date of the Note.',
+ nullable: true
+ })
+ public scheduledAt: Date | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public reason: string | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of author.',
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ @Column('jsonb')
+ public draft: NoteCreateOption;
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 800af98b1..7bf282a18 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -34,6 +34,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js';
+import { MiScheduledNote } from '@/models/ScheduledNote.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
@@ -108,6 +109,7 @@ export {
MiMuting,
MiRenoteMuting,
MiNote,
+ MiScheduledNote,
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
@@ -181,6 +183,7 @@ export type ModerationLogsRepository = Repository;
export type MutingsRepository = Repository;
export type RenoteMutingsRepository = Repository;
export type NotesRepository = Repository;
+export type ScheduledNotesRepository = Repository;
export type NoteFavoritesRepository = Repository;
export type NoteReactionsRepository = Repository;
export type NoteThreadMutingsRepository = Repository;
diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts
new file mode 100644
index 000000000..7b81838fe
--- /dev/null
+++ b/packages/backend/src/models/json-schema/note-draft.ts
@@ -0,0 +1,179 @@
+export const packedNoteDraftSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id'
+ },
+ updatedAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time'
+ },
+ scheduledAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
+ reason: {
+ type: 'string',
+ optional: true, nullable: false
+ },
+ channel: {
+ type: 'object',
+ optional: true, nullable: true,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id'
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false
+ },
+ },
+ },
+ renote: {
+ type: 'object',
+ optional: true, nullable: true,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ text: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ username: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ host: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ },
+ },
+ },
+ },
+ reply: {
+ type: 'object',
+ optional: true, nullable: true,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ text: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id'
+ },
+ username: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ host: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ },
+ },
+ },
+ },
+ data: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ text: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ useCw: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ cw: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ visibility: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['public', 'home', 'followers', 'specified'],
+ },
+ localOnly: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ files: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'DriveFile',
+ },
+ },
+ poll: {
+ type: 'object',
+ optional: false, nullable: true,
+ properties: {
+ choices: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ multiple: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ expiresAt: {
+ type: 'integer',
+ optional: false, nullable: true,
+ },
+ expiredAfter: {
+ type: 'integer',
+ optional: false, nullable: true,
+ minimum: 1
+ },
+ },
+ },
+ visibleUserIds: {
+ type: 'array',
+ optional: true, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ },
+ },
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index b4c444275..e68240897 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -296,6 +296,51 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['noteScheduled'],
+ },
+ draft: {
+ type: 'object',
+ ref: 'NoteDraft',
+ optional: false, nullable: false,
+ }
+ }
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['scheduledNotePosted'],
+ },
+ note: {
+ type: 'object',
+ ref: 'Note',
+ optional: false, nullable: false,
+ }
+ }
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['scheduledNoteError'],
+ },
+ draft: {
+ type: 'object',
+ ref: 'NoteDraft',
+ optional: false, nullable: false,
+ }
+ }
}, {
type: 'object',
properties: {
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 166a085f5..6cbea1708 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -180,6 +180,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ canScheduleNote: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
canInitiateConversation: {
type: 'boolean',
optional: false, nullable: false,
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 58a40ae3c..b272f7d65 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -44,6 +44,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js';
+import { MiScheduledNote } from '@/models/ScheduledNote.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
@@ -162,6 +163,7 @@ export const entities = [
MiRenoteMuting,
MiBlocking,
MiNote,
+ MiScheduledNote,
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index f3588544f..d7f0a56e7 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
@@ -36,6 +37,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
+import { ScheduledNoteProcessorService } from './processors/ScheduledNoteProcessorService.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
@@ -52,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ResyncChartsProcessorService,
CleanChartsProcessorService,
CheckExpiredMutingsProcessorService,
+ CheckMissingScheduledNoteProcessorService,
CleanProcessorService,
DeleteDriveFilesProcessorService,
ExportCustomEmojisProcessorService,
@@ -75,6 +78,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
CleanRemoteFilesProcessorService,
RelationshipProcessorService,
ReportAbuseProcessorService,
+ ScheduledNoteProcessorService,
WebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
DeliverProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 1e68f750a..dc1429944 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -35,10 +35,12 @@ import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesP
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
-import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
+import { ScheduledNoteProcessorService } from './processors/ScheduledNoteProcessorService.js';
+import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
@@ -113,11 +115,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
private relationshipProcessorService: RelationshipProcessorService,
private reportAbuseProcessorService: ReportAbuseProcessorService,
- private tickChartsProcessorService: TickChartsProcessorService,
private resyncChartsProcessorService: ResyncChartsProcessorService,
+ private scheduledNoteProcessorService: ScheduledNoteProcessorService,
+ private tickChartsProcessorService: TickChartsProcessorService,
private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
+ private checkMissingScheduledNoteProcessorService: CheckMissingScheduledNoteProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
@@ -141,11 +145,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region system
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
switch (job.name) {
+ case 'scheduledNote': return this.scheduledNoteProcessorService.process(job);
case 'tickCharts': return this.tickChartsProcessorService.process();
case 'resyncCharts': return this.resyncChartsProcessorService.process();
case 'cleanCharts': return this.cleanChartsProcessorService.process();
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'checkMissingScheduledNote': return this.checkMissingScheduledNoteProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
diff --git a/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts
new file mode 100644
index 000000000..8c441a9cc
--- /dev/null
+++ b/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts
@@ -0,0 +1,61 @@
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DI } from '@/di-symbols.js';
+import type { ScheduledNotesRepository } from '@/models/_.js';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { acquireDistributedLock } from '@/misc/distributed-lock.js';
+import { QueueService } from '@/core/QueueService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+
+@Injectable()
+export class CheckMissingScheduledNoteProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.redisForTimelines)
+ private redisForTimelines: Redis.Redis,
+
+ @Inject(DI.scheduledNotesRepository)
+ private scheduledNotesRepository: ScheduledNotesRepository,
+
+ private queueService: QueueService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('note:scheduled');
+ }
+
+ @bindThis
+ public async process(): Promise {
+ this.logger.info(`checking missing scheduled note tasks`);
+
+ try {
+ await acquireDistributedLock(this.redisForTimelines, `note:scheduled:check`, 3 * 60 * 1000, 1, 1000);
+ } catch (e) {
+ this.logger.warn(`check is already being processed`);
+ return;
+ }
+
+ const query = this.scheduledNotesRepository.createQueryBuilder('draft')
+ .where('draft.scheduledAt < now() - interval \'5 minutes\'').orderBy('draft.createdAt', 'ASC');
+
+ let lastId = '0';
+ while (true) {
+ const drafts = await query.andWhere('draft.id > :lastId', { lastId }).limit(100).getMany();
+
+ if (drafts.length === 0) {
+ break;
+ }
+
+ for (const draft of drafts.filter(draft => draft.scheduledAt !== null)) {
+ const jobState = await this.queueService.systemQueue.getJobState(`scheduledNote:${draft.id}`);
+ if (jobState !== 'unknown') continue;
+
+ this.logger.warn(`found missing scheduled note task: ${draft.id}`);
+ await this.queueService.createScheduledNoteJob(draft.id, draft.scheduledAt!);
+ }
+
+ lastId = drafts[drafts.length - 1].id;
+ }
+ }
+}
diff --git a/packages/backend/src/queue/processors/ScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/ScheduledNoteProcessorService.ts
new file mode 100644
index 000000000..3ca28941e
--- /dev/null
+++ b/packages/backend/src/queue/processors/ScheduledNoteProcessorService.ts
@@ -0,0 +1,101 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import * as Bull from 'bullmq';
+import * as Redis from 'ioredis';
+import { DI } from '@/di-symbols.js';
+import { MiNote, type ScheduledNotesRepository } from '@/models/_.js';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { acquireDistributedLock } from '@/misc/distributed-lock.js';
+import { NotificationService } from '@/core/NotificationService.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type { ScheduledNoteJobData } from '../types.js';
+
+@Injectable()
+export class ScheduledNoteProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.redisForTimelines)
+ private redisForTimelines: Redis.Redis,
+
+ @Inject(DI.scheduledNotesRepository)
+ private scheduledNotesRepository: ScheduledNotesRepository,
+
+ private notificationService: NotificationService,
+ private noteCreateService: NoteCreateService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('note:scheduled');
+ }
+
+ @bindThis
+ public async process(job: Bull.Job): Promise {
+ this.logger.info(`processing ${job.data.draftId}`);
+
+ try {
+ await acquireDistributedLock(this.redisForTimelines, `note:scheduled:${job.data.draftId}`, 30 * 1000, 1, 100);
+ } catch (e) {
+ this.logger.warn(`draft=${job.data.draftId} is already being processed`);
+ return 'ok';
+ }
+
+ const draft = await this.scheduledNotesRepository.findOne({
+ where: { id: job.data.draftId, reason: IsNull() },
+ relations: ['user'],
+ });
+
+ if (draft == null) {
+ this.logger.warn(`draft not found: ${job.data.draftId}`);
+ return 'ok';
+ }
+
+ if (!draft.user || draft.user.isSuspended) {
+ this.logger.warn(`user is suspended: ${draft.userId}`);
+ await this.scheduledNotesRepository.delete({ id: draft.id });
+ return 'ok';
+ }
+
+ try {
+ const note = (await this.noteCreateService.create(draft.user, {
+ ...draft.draft,
+ createdAt: new Date(),
+ scheduledAt: null,
+ })) as MiNote;
+
+ await this.scheduledNotesRepository.delete({ id: draft.id });
+
+ this.notificationService.createNotification(draft.userId, "scheduledNotePosted", {
+ noteId: note.id,
+ });
+
+ return 'ok';
+ } catch (e) {
+ if (e instanceof IdentifiableError) {
+ if ([
+ 'e11b3a16-f543-4885-8eb1-66cad131dbfd',
+ '689ee33f-f97c-479a-ac49-1b9f8140af99',
+ '9f466dab-c856-48cd-9e65-ff90ff750580',
+ '85ab9bd7-3a41-4530-959d-f07073900109',
+ 'd450b8a9-48e4-4dab-ae36-f4db763fda7c',
+ ].includes(e.id)) {
+ this.logger.warn(`creating note from draft=${draft.id} failed: ${e.message}`);
+
+ await this.scheduledNotesRepository.update({ id: draft.id }, {
+ scheduledAt: null,
+ reason: e.message,
+ });
+
+ this.notificationService.createNotification(draft.userId, "scheduledNoteError", {
+ draftId: draft.id,
+ });
+
+ return e.message;
+ }
+ }
+ throw e;
+ }
+ }
+}
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 49aedec58..ca2662155 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -6,6 +6,7 @@
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
+import type { MiScheduledNote } from '@/models/ScheduledNote.js';
import type { MiUser } from '@/models/User.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiWebhook } from '@/models/Webhook.js';
@@ -116,6 +117,10 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
+export type ScheduledNoteJobData = {
+ draftId: MiScheduledNote['id'];
+};
+
export type WebhookDeliverJobData = {
type: string;
content: unknown;
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 924eac7a1..33e93a814 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -287,6 +287,8 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
+import * as ep___notes_scheduled_cancel from './endpoints/notes/scheduled/cancel.js';
+import * as ep___notes_scheduled_list from './endpoints/notes/scheduled/list.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
@@ -682,6 +684,8 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
+const $notes_scheduled_cancel: Provider = { provide: 'ep:notes/scheduled/cancel', useClass: ep___notes_scheduled_cancel.default };
+const $notes_scheduled_list: Provider = { provide: 'ep:notes/scheduled/list', useClass: ep___notes_scheduled_list.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
@@ -1081,6 +1085,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_conversation,
$notes_create,
$notes_delete,
+ $notes_scheduled_cancel,
+ $notes_scheduled_list,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
@@ -1474,6 +1480,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_conversation,
$notes_create,
$notes_delete,
+ $notes_scheduled_cancel,
+ $notes_scheduled_list,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 987228be6..a5f51328c 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -287,6 +287,8 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
+import * as ep___notes_scheduled_cancel from './endpoints/notes/scheduled/cancel.js';
+import * as ep___notes_scheduled_list from './endpoints/notes/scheduled/list.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
@@ -680,6 +682,8 @@ const eps = [
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/delete', ep___notes_delete],
+ ['notes/scheduled/cancel', ep___notes_scheduled_cancel],
+ ['notes/scheduled/list', ep___notes_scheduled_list],
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 963f3bbfd..c90c9daf9 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,6 +17,7 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -148,6 +149,31 @@ export const meta = {
id: '66819f28-9525-389d-4b0a-4974363fbbbf',
},
+ cannotScheduleToPast: {
+ message: 'Cannot schedule to the past.',
+ code: 'CANNOT_SCHEDULE_TO_PAST',
+ 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',
+ },
+
+ rolePermissionDenied: {
+ message: 'You are not assigned to a required role.',
+ code: 'ROLE_PERMISSION_DENIED',
+ kind: 'permission',
+ id: '12f1d5d2-f7ec-4d7c-b608-e873f4b20327',
+ status: 403,
+ },
},
} as const;
@@ -207,6 +233,7 @@ export const paramDef = {
},
required: ['choices'],
},
+ scheduledAt: { type: 'integer', nullable: true },
noCreatedNote: { type: 'boolean', default: false },
},
// (re)note with text, files and poll are optional
@@ -263,6 +290,7 @@ export default class extends Endpoint { // eslint-
private loggerService: LoggerService,
private noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService,
+ private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
const logger = this.loggerService.getLogger('api:notes:create');
@@ -318,7 +346,7 @@ export default class extends Endpoint { // eslint-
let renote: MiNote | null = null;
if (ps.renoteId != null) {
// Fetch renote to note
- renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
+ renote = await this.notesRepository.findOne({ where: { id: ps.renoteId }, relations: ['user'] });
if (renote == null) {
logger.error('No such renote target.', { renoteId: ps.renoteId });
@@ -371,7 +399,7 @@ export default class extends Endpoint { // eslint-
let reply: MiNote | null = null;
if (ps.replyId != null) {
// Fetch reply
- reply = await this.notesRepository.findOneBy({ id: ps.replyId });
+ reply = await this.notesRepository.findOne({ where: { id: ps.replyId }, relations: ['user'] });
if (reply == null) {
logger.error('No such reply target.', { replyId: ps.replyId });
@@ -384,11 +412,8 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
- } else if ( me.isBot ) {
- const replayuser = await this.usersRepository.findOneBy({ id: reply.userId });
- if (replayuser?.isBot) {
- throw new ApiError(meta.errors.replyingToAnotherBot);
- }
+ } else if (me.isBot && reply.user!.isBot) {
+ throw new ApiError(meta.errors.replyingToAnotherBot);
}
// Check blocking
@@ -427,10 +452,28 @@ export default class extends Endpoint { // eslint-
}
}
+ let scheduledAt: Date | null = null;
+ if (ps.scheduledAt) {
+ const now = new Date();
+ scheduledAt = new Date(ps.scheduledAt);
+ scheduledAt.setMilliseconds(0);
+
+ if (scheduledAt < now) {
+ 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);
+ }
+ }
+
// 投稿を作成
try {
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
+ scheduledAt: ps.scheduledAt ? scheduledAt : null,
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
@@ -454,10 +497,18 @@ export default class extends Endpoint { // eslint-
// 1分間、リクエストの処理結果を記録
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
- logger.info('Successfully created a note.', { noteId: note.id });
- if (ps.noCreatedNote) return;
+ if (!scheduledAt) {
+ logger.info('Successfully created a note.', { noteId: note.id });
+ } else {
+ this.notificationService.createNotification(me.id, "noteScheduled", {
+ draftId: note.id,
+ });
+ logger.info('Successfully scheduled a note.', { draftId: note.id });
+ }
+
+ if (ps.noCreatedNote || scheduledAt) return;
else return {
- createdNote: await this.noteEntityService.pack(note, me),
+ createdNote: await this.noteEntityService.pack(note as MiNote, me),
};
} catch (err) {
// エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除
@@ -468,6 +519,8 @@ export default class extends Endpoint { // eslint-
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);
}
throw err;
diff --git a/packages/backend/src/server/api/endpoints/notes/scheduled/cancel.ts b/packages/backend/src/server/api/endpoints/notes/scheduled/cancel.ts
new file mode 100644
index 000000000..b3627521e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/scheduled/cancel.ts
@@ -0,0 +1,57 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import type { ScheduledNotesRepository } from '@/models/_.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ApiError } from '@/server/api/error.js';
+import ms from 'ms';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canCreateContent',
+
+ prohibitMoved: true,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 300,
+ },
+
+ kind: 'write:notes',
+
+ errors: {
+ noSuchDraft: {
+ message: 'No such draft',
+ code: 'NO_SUCH_DRAFT',
+ id: '91c2ad21-fb45-4f2a-ba4c-ea749b262947',
+ }
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ draftId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['draftId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.scheduledNotesRepository)
+ private scheduledNotesRepository: ScheduledNotesRepository,
+
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const draft = await this.scheduledNotesRepository.findOneBy({ id: ps.draftId, userId: me.id });
+ if (!draft) throw new ApiError(meta.errors.noSuchDraft);
+
+ await this.queueService.systemQueue.remove(`scheduledNote:${draft.id}`);
+ await this.scheduledNotesRepository.delete({ id: draft.id });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/scheduled/list.ts b/packages/backend/src/server/api/endpoints/notes/scheduled/list.ts
new file mode 100644
index 000000000..c27a7b9a7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/scheduled/list.ts
@@ -0,0 +1,49 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import type { ScheduledNotesRepository } from '@/models/_.js';
+import { ScheduledNoteEntityService } from '@/core/entities/ScheduledNoteEntityService.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canScheduleNote',
+
+ kind: 'write:notes',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'NoteDraft',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', default: 0 },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.scheduledNotesRepository)
+ private scheduledNotesRepository: ScheduledNotesRepository,
+
+ private scheduledNoteEntityService: ScheduledNoteEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.scheduledNotesRepository.createQueryBuilder('draft').where('draft.userId = :userId', { userId: me.id });
+ const drafts = await query.orderBy('draft.scheduledAt', 'ASC', 'NULLS FIRST').offset(ps.offset).limit(ps.limit).getMany();
+
+ return await this.scheduledNoteEntityService.packMany(drafts, me);
+ });
+ }
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index e125b074f..945eb27b5 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -3,6 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { MiDriveFile } from '@/models/DriveFile.js';
+import type { IPoll } from '@/models/Poll.js';
+import type { MiChannel } from '@/models/Channel.js';
+import type { MiApp } from '@/models/App.js';
+import type { MiUser } from '@/models/User.js';
+import type { MiNote } from '@/models/Note.js';
+
/**
* note - 通知オンにしているユーザーが投稿した
* follow - フォローされた
@@ -16,6 +23,9 @@
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* roleAssigned - ロールが付与された
* achievementEarned - 実績を獲得
+ * noteScheduled - 予約投稿が予約された
+ * scheduledNotePosted - 予約投稿が投稿された
+ * scheduledNoteError - 予約投稿がエラーになった
* app - アプリ通知
* test - テスト通知(サーバー側)
*/
@@ -32,6 +42,9 @@ export const notificationTypes = [
'followRequestAccepted',
'roleAssigned',
'achievementEarned',
+ 'noteScheduled',
+ 'scheduledNotePosted',
+ 'scheduledNoteError',
'app',
'test',
] as const;
@@ -338,6 +351,36 @@ export type ModerationLogPayloads = {
}
};
+export type MinimumUser = {
+ id: MiUser['id'];
+ host: MiUser['host'];
+ username: MiUser['username'];
+ uri: MiUser['uri'];
+};
+
+export type NoteCreateOption = {
+ createdAt?: Date | null;
+ scheduledAt?: Date | null;
+ name?: string | null;
+ text?: string | null;
+ reply?: MiNote | null;
+ renote?: MiNote | null;
+ files?: MiDriveFile[] | null;
+ poll?: IPoll | null;
+ localOnly?: boolean | null;
+ reactionAcceptance?: MiNote['reactionAcceptance'];
+ cw?: string | null;
+ visibility?: string;
+ visibleUsers?: MinimumUser[] | null;
+ channel?: MiChannel | null;
+ apMentions?: MinimumUser[] | null;
+ apHashtags?: string[] | null;
+ apEmojis?: string[] | null;
+ uri?: string | null;
+ url?: string | null;
+ app?: MiApp | null;
+};
+
export type Serialized = {
[K in keyof T]:
T[K] extends Date
diff --git a/packages/frontend/src/components/MkDraftsDialog.vue b/packages/frontend/src/components/MkDraftsDialog.vue
index 755bb2cf1..289707cd2 100644
--- a/packages/frontend/src/components/MkDraftsDialog.vue
+++ b/packages/frontend/src/components/MkDraftsDialog.vue
@@ -10,14 +10,18 @@
{{ i18n.ts.drafts }}
-
+
+
+
+
+
{{ i18n.ts.nothing }}
-
+
+
+
+
+
![]()
+
{{ i18n.ts.nothing }}
+
+
+
+
+
+
+
+
+ {{ draft.channel.name }}
+
+
+ {{ draft.renote.text }}
+
+
+ {{ draft.reply.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.error }}: {{ draft.reason }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 1450a3022..f40dc1b2b 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -25,6 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
+ [$style.t_noteScheduled]: notification.type === 'noteScheduled',
+ [$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
+ [$style.t_scheduledNoteError]: notification.type === 'scheduledNoteError',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]"
>
@@ -37,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
@@ -52,16 +58,19 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._notification.pollEnded }}
- {{ i18n.ts._notification.newNote }}:
- {{ i18n.ts._notification.roleAssigned }}
- {{ i18n.ts._notification.achievementEarned }}
- {{ i18n.ts._notification.testNotification }}
+ {{ i18n.ts._notification.pollEnded }}
+ {{ i18n.ts._notification.newNote }}:
+ {{ i18n.ts._notification.roleAssigned }}
+ {{ i18n.ts._notification.achievementEarned }}
+ {{ i18n.ts._notification.noteScheduled }}
+ {{ i18n.ts._notification.scheduledNotePosted }}
+ {{ i18n.ts._notification.scheduledNoteError }}
+ {{ i18n.ts._notification.testNotification }}
- {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
- {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
- {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}
- {{ notification.header }}
+ {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
+ {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
+ {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}
+ {{ notification.header }}
@@ -98,6 +107,23 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
+
+
+
+
+
+
+
+
+ {{ notification.draft.reason }}
+
+
{{ i18n.ts.youGotNewFollower }}
@@ -300,6 +326,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
+.t_noteScheduled, .t_scheduledNotePosted, .t_scheduledNoteError {
+ padding: 3px;
+ background: var(--eventOther);
+ pointer-events: none;
+}
+
.t_roleAssigned {
padding: 3px;
background: var(--eventOther);
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 9911dc0ae..ddf4a157d 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -81,6 +81,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ maxTextLength - textLength }}
+
+
+
{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
+
+
@@ -94,6 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -115,7 +121,6 @@ import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js';
-import type { NoteDraftItem } from '@/types/note-draft-item.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
@@ -138,8 +143,8 @@ import { uploadFile } from '@/scripts/upload.js';
import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
+import { dateTimeFormat } from '@/scripts/intl-const.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
const $i = signinRequired();
@@ -211,6 +216,7 @@ if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
}
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
+const scheduledTime = ref
(null);
const autocompleteTextareaInput = ref(null);
const autocompleteCwInput = ref(null);
const autocompleteHashtagsInput = ref(null);
@@ -259,11 +265,15 @@ const placeholder = computed((): string => {
});
const submitText = computed((): string => {
- return renote.value
- ? i18n.ts.quote
- : reply.value
- ? i18n.ts.reply
- : i18n.ts.note;
+ if (scheduledTime.value) {
+ return i18n.ts.schedule;
+ } else if (renote.value) {
+ return i18n.ts.quote;
+ } else if (reply.value) {
+ return i18n.ts.reply;
+ } else {
+ return i18n.ts.note;
+ }
});
const textLength = computed((): number => {
@@ -389,6 +399,7 @@ function watchForDraft() {
watch(files, () => saveDraft(), { deep: true });
watch(visibility, () => saveDraft());
watch(localOnly, () => saveDraft());
+ watch(scheduledTime, () => saveDraft());
}
function checkMissingMention() {
@@ -583,10 +594,25 @@ function removeVisibleUser(user) {
visibleUsers.value = erase(user, visibleUsers.value);
}
+async function setScheduledTime() {
+ const { canceled, result: date } = await os.inputDateTime({
+ title: i18n.ts.setScheduledTime,
+ });
+ if (canceled) return;
+
+ scheduledTime.value = date;
+}
+
function clear() {
text.value = '';
+ useCw.value = false;
+ cw.value = null;
+ visibility.value = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
+ localOnly.value = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
files.value = [];
poll.value = null;
+ visibleUsers.value = [];
+ scheduledTime.value = null;
quoteId.value = null;
}
@@ -694,10 +720,16 @@ function onDrop(ev: DragEvent): void {
function saveDraft() {
if (props.instant || props.mock) return;
- const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
+ let scheduledAt = scheduledTime.value ?? null;
+ if (scheduledAt && (isNaN(scheduledAt.getTime()) || scheduledAt.getTime() < Date.now())) {
+ scheduledAt = null;
+ }
+
+ const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
draftData[draftKey.value] = {
updatedAt: new Date().toISOString(),
+ scheduledAt: scheduledAt?.toISOString() ?? null,
channel: channel.value ? {
id: channel.value.id,
name: channel.value.name,
@@ -737,7 +769,7 @@ function saveDraft() {
}
function deleteDraft() {
- const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
+ const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
delete draftData[draftKey.value];
@@ -777,7 +809,7 @@ async function openDrafts() {
}
function loadDraft(exactMatch = false) {
- const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
+ const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record;
const scope = exactMatch ? draftKey.value : draftKey.value.replace(`note:${draftId.value}`, 'note:');
const draft = Object.entries(drafts).filter(([k]) => k.startsWith(scope))
.map(r => ({ key: r[0], value: { ...r[1], updatedAt: new Date(r[1].updatedAt).getTime() } }))
@@ -788,7 +820,11 @@ function loadDraft(exactMatch = false) {
draftId.value = draft.key.replace(scope, '');
}
- text.value = draft.value.data.text;
+ scheduledTime.value = draft.value.scheduledAt ? new Date(draft.value.scheduledAt) : null;
+ if (scheduledTime.value && (isNaN(scheduledTime.value.getTime()) || scheduledTime.value.getTime() < Date.now())) {
+ scheduledTime.value = null;
+ }
+ text.value = draft.value.data.text ?? '';
useCw.value = draft.value.data.useCw;
cw.value = draft.value.data.cw;
visibility.value = draft.value.data.visibility;
@@ -872,6 +908,7 @@ async function post(ev?: MouseEvent) {
visibility: visibility.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance: reactionAcceptance.value,
+ scheduledAt: scheduledTime.value?.getTime() ?? undefined,
noCreatedNote: true,
};
@@ -1079,6 +1116,7 @@ onMounted(() => {
visibility.value = init.visibility;
localOnly.value = init.localOnly ?? false;
quoteId.value = init.renote ? init.renote.id : null;
+ scheduledTime.value = null;
}
nextTick(() => watchForDraft());
@@ -1352,6 +1390,13 @@ defineExpose({
}
}
+.scheduledTime {
+ display: flex;
+ padding: 8px 24px;
+ gap: 4px;
+ background: var(--infoBg);
+}
+
.footer {
display: flex;
padding: 0 16px 16px 16px;
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 32b0bb353..1483b9d0c 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -67,6 +67,9 @@ export const notificationTypes = [
'followRequestAccepted',
'roleAssigned',
'achievementEarned',
+ 'noteScheduled',
+ 'scheduledNotePosted',
+ 'scheduledNoteError',
'app',
] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
@@ -75,6 +78,7 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
+ 'canScheduleNote',
'canInitiateConversation',
'canCreateContent',
'canUpdateContent',
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 5560335d8..9a5243926 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -427,28 +427,36 @@ export function inputNumber(props: {
});
}
-export function inputDate(props: {
+export function inputDateTime(props: {
title?: string | null;
text?: string | null;
placeholder?: string | null;
- default?: string | null;
+ default?: Date | null;
}): Promise<{
canceled: true; result: undefined;
} | {
canceled: false; result: Date;
}> {
+ const defaultValue = props.default ?? new Date();
+ defaultValue.setMinutes(defaultValue.getMinutes() - defaultValue.getTimezoneOffset());
+
return new Promise(resolve => {
popup(MkDialog, {
title: props.title ?? undefined,
text: props.text ?? undefined,
input: {
- type: 'date',
+ type: 'datetime-local',
placeholder: props.placeholder,
- default: props.default ?? null,
+ default: defaultValue.toISOString().slice(0, -5),
},
}, {
done: result => {
- resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
+ const date = result ? new Date(result.result) : undefined;
+ if (date && !isNaN(date.getTime())) {
+ resolve({ result: date, canceled: false });
+ } else {
+ resolve({ result: undefined, canceled: true });
+ }
},
}, 'closed');
});
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 2665bfb90..d4d5b5050 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -165,6 +165,26 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canScheduleNote }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canScheduleNote.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
{{ i18n.ts._role._options.canInitiateConversation }}
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 62782ef75..132ecbd31 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -48,6 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canScheduleNote }}
+ {{ policies.canScheduleNote ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
{{ i18n.ts._role._options.canInitiateConversation }}
{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 273250d1d..b9e804c3f 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -57,7 +57,7 @@ function top() {
}
async function timetravel() {
- const { canceled, result: date } = await os.inputDate({
+ const { canceled, result: date } = await os.inputDateTime({
title: i18n.ts.date,
});
if (canceled) return;
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 75419ce06..311493aa8 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -221,7 +221,7 @@ function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue:
}
async function timetravel(): Promise {
- const { canceled, result: date } = await os.inputDate({
+ const { canceled, result: date } = await os.inputDateTime({
title: i18n.ts.date,
});
if (canceled) return;
diff --git a/packages/frontend/src/types/note-draft-item.ts b/packages/frontend/src/types/note-draft-item.ts
deleted file mode 100644
index a7ec5962e..000000000
--- a/packages/frontend/src/types/note-draft-item.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as Misskey from 'misskey-js';
-import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
-
-export type NoteDraftItem = {
- updatedAt: string;
- channel?: {
- id: string;
- name: string;
- };
- renote?: {
- id: string;
- text: string | null;
- user: {
- id: string;
- username: string;
- host: string | null;
- };
- };
- reply?: {
- id: string;
- text: string | null;
- user: {
- id: string;
- username: string;
- host: string | null;
- };
- };
- data: {
- text: string;
- useCw: boolean;
- cw: string | null;
- visibility: 'public' | 'followers' | 'home' | 'specified';
- localOnly: boolean;
- files: Misskey.entities.DriveFile[];
- poll: PollEditorModelValue | null;
- visibleUserIds?: string[];
- };
-};
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 4c03556e6..27d102bd7 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1640,6 +1640,9 @@ declare namespace entities {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
+ NotesScheduledCancelRequest,
+ NotesScheduledListRequest,
+ NotesScheduledListResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -1825,6 +1828,7 @@ declare namespace entities {
Announcement,
App,
Note,
+ NoteDraft,
NoteReaction,
NoteFavorite,
Notification_2 as Notification,
@@ -2573,6 +2577,9 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
// @public (undocumented)
type Note = components['schemas']['Note'];
+// @public (undocumented)
+type NoteDraft = components['schemas']['NoteDraft'];
+
// @public (undocumented)
type NoteFavorite = components['schemas']['NoteFavorite'];
@@ -2681,6 +2688,15 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j
// @public (undocumented)
type NotesResponse = operations['notes']['responses']['200']['content']['application/json'];
+// @public (undocumented)
+type NotesScheduledCancelRequest = operations['notes___scheduled___cancel']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type NotesScheduledListRequest = operations['notes___scheduled___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type NotesScheduledListResponse = operations['notes___scheduled___list']['responses']['200']['content']['application/json'];
+
// @public (undocumented)
type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
@@ -2742,7 +2758,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
-export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
+export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "noteScheduled", "scheduledNotePosted", "scheduledNoteError"];
// @public (undocumented)
type Page = components['schemas']['Page'];
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 0b8ee2473..712755926 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -3129,6 +3129,28 @@ declare module '../api.js' {
credential?: string | null,
): Promise>;
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 246d2c99b..ab44ed7d4 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -418,6 +418,9 @@ import type {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
+ NotesScheduledCancelRequest,
+ NotesScheduledListRequest,
+ NotesScheduledListResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -872,6 +875,8 @@ export type Endpoints = {
'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse };
'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse };
'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse };
+ 'notes/scheduled/cancel': { req: NotesScheduledCancelRequest; res: EmptyResponse };
+ 'notes/scheduled/list': { req: NotesScheduledListRequest; res: NotesScheduledListResponse };
'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse };
'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse };
'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index b26acb1fa..b74926c87 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -421,6 +421,9 @@ export type NotesConversationResponse = operations['notes___conversation']['resp
export type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json'];
export type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json'];
export type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
+export type NotesScheduledCancelRequest = operations['notes___scheduled___cancel']['requestBody']['content']['application/json'];
+export type NotesScheduledListRequest = operations['notes___scheduled___list']['requestBody']['content']['application/json'];
+export type NotesScheduledListResponse = operations['notes___scheduled___list']['responses']['200']['content']['application/json'];
export type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];
export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json'];
export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index e60fe35aa..332eb28bf 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -14,6 +14,7 @@ export type Ad = components['schemas']['Ad'];
export type Announcement = components['schemas']['Announcement'];
export type App = components['schemas']['App'];
export type Note = components['schemas']['Note'];
+export type NoteDraft = components['schemas']['NoteDraft'];
export type NoteReaction = components['schemas']['NoteReaction'];
export type NoteFavorite = components['schemas']['NoteFavorite'];
export type Notification = components['schemas']['Notification'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 63e97c0f6..b0d2d7662 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -2709,6 +2709,24 @@ export type paths = {
*/
post: operations['notes___delete'];
};
+ '/notes/scheduled/cancel': {
+ /**
+ * notes/scheduled/cancel
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ post: operations['notes___scheduled___cancel'];
+ };
+ '/notes/scheduled/list': {
+ /**
+ * notes/scheduled/list
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ post: operations['notes___scheduled___list'];
+ };
'/notes/favorites/create': {
/**
* notes/favorites/create
@@ -4272,6 +4290,58 @@ export type components = {
clippedCount?: number;
myReaction?: string | null;
};
+ NoteDraft: {
+ /** Format: misskey:id */
+ id: string;
+ /** Format: date-time */
+ updatedAt: string;
+ /** Format: date-time */
+ scheduledAt: string | null;
+ reason?: string;
+ channel?: {
+ /** Format: misskey:id */
+ id: string;
+ name: string;
+ } | null;
+ renote?: ({
+ /** Format: misskey:id */
+ id: string;
+ text: string | null;
+ user: {
+ /** Format: misskey:id */
+ id: string;
+ username: string;
+ host: string | null;
+ };
+ }) | null;
+ reply?: ({
+ /** Format: misskey:id */
+ id: string;
+ text: string | null;
+ user: {
+ /** Format: misskey:id */
+ id: string;
+ username: string;
+ host: string | null;
+ };
+ }) | null;
+ data: {
+ text: string | null;
+ useCw: boolean;
+ cw: string | null;
+ /** @enum {string} */
+ visibility: 'public' | 'home' | 'followers' | 'specified';
+ localOnly: boolean;
+ files: components['schemas']['DriveFile'][];
+ poll: ({
+ choices: string[];
+ multiple: boolean;
+ expiresAt: number | null;
+ expiredAfter: number | null;
+ }) | null;
+ visibleUserIds?: string[];
+ };
+ };
NoteReaction: {
/**
* Format: id
@@ -4419,6 +4489,30 @@ export type components = {
/** @enum {string} */
type: 'achievementEarned';
achievement: string;
+ } | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
+ type: 'noteScheduled';
+ draft: components['schemas']['NoteDraft'];
+ } | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
+ type: 'scheduledNotePosted';
+ note: components['schemas']['Note'];
+ } | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
+ type: 'scheduledNoteError';
+ draft: components['schemas']['NoteDraft'];
} | {
/** Format: id */
id: string;
@@ -4983,6 +5077,7 @@ export type components = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
+ canScheduleNote: boolean;
canInitiateConversation: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
@@ -20366,8 +20461,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
};
};
};
@@ -20434,8 +20529,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
};
};
};
@@ -23333,6 +23428,7 @@ export type operations = {
expiresAt?: number | null;
expiredAfter?: number | null;
}) | null;
+ scheduledAt?: number | null;
/** @default false */
noCreatedNote?: boolean;
};
@@ -23447,6 +23543,120 @@ export type operations = {
};
};
};
+ /**
+ * notes/scheduled/cancel
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ notes___scheduled___cancel: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ draftId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description To many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * notes/scheduled/list
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ notes___scheduled___list: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 10 */
+ limit?: number;
+ /** @default 0 */
+ offset?: number;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['NoteDraft'][];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* notes/favorites/create
* @description No description provided.
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index d915224a9..5fcad5f46 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -1,4 +1,4 @@
-export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
+export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index 7d28d8a69..1edd528f2 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -39,7 +39,7 @@ export async function createNotification {
+async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions & { actions?: Record[], renotify?: boolean }] | null> {
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
const { t } = i18n;
switch (data.type) {
@@ -60,7 +60,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
return [t('_notification.youWereFollowed'), {
body: getUserName(data.body.user),
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('user-plus'),
data,
actions: userDetail.isFollowing ? [] : [
@@ -75,7 +75,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'mention':
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('at'),
data,
actions: [
@@ -89,7 +89,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'reply':
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('arrow-back-up'),
data,
actions: [
@@ -103,7 +103,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'renote':
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('repeat'),
data,
actions: [
@@ -117,7 +117,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'quote':
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('quote'),
data,
actions: [
@@ -137,7 +137,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'note':
return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
data,
}];
@@ -164,7 +164,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
const tag = `reaction:${data.body.note.id}`;
return [`${reaction} ${getUserName(data.body.user)}`, {
body: data.body.note.text ?? '',
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
tag,
badge,
data,
@@ -180,7 +180,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'receiveFollowRequest':
return [t('_notification.youReceivedFollowRequest'), {
body: getUserName(data.body.user),
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('user-plus'),
data,
actions: [
@@ -198,11 +198,18 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'followRequestAccepted':
return [t('_notification.yourFollowRequestAccepted'), {
body: getUserName(data.body.user),
- icon: data.body.user.avatarUrl,
+ icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('circle-check'),
data,
}];
+ case 'pollEnded':
+ return [t('_notification.pollEnded'), {
+ body: data.body.note.text ?? '',
+ badge: iconUrl('chart-arrows'),
+ data,
+ }];
+
case 'achievementEarned':
return [t('_notification.achievementEarned'), {
body: t(`_achievements._types._${data.body.achievement}.title`),
@@ -211,10 +218,32 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
tag: `achievement:${data.body.achievement}`,
}];
- case 'pollEnded':
- return [t('_notification.pollEnded'), {
+ case 'noteScheduled':
+ return [t('_notification.noteScheduled'), {
+ body: data.body.draft.data.text ?? '',
+ badge: iconUrl('calendar-clock'),
+ data,
+ }];
+
+ case 'scheduledNotePosted':
+ return [t('_notification.scheduledNotePosted'), {
body: data.body.note.text ?? '',
- badge: iconUrl('chart-arrows'),
+ badge: iconUrl('calendar-check'),
+ data,
+ }];
+
+ case 'scheduledNoteError':
+ return [t('_notification.scheduledNoteError'), {
+ body: data.body.draft.reason ?? '',
+ badge: iconUrl('calendar-exclamation'),
+ data,
+ }];
+
+ case 'roleAssigned':
+ return [t('_notification.roleAssigned'), {
+ body: data.body.role.name,
+ icon: data.body.role.iconUrl ?? undefined,
+ badge: iconUrl('badges'),
data,
}];
@@ -238,7 +267,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
case 'unreadAntennaNote':
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
- icon: data.body.note.user.avatarUrl,
+ icon: data.body.note.user.avatarUrl ?? undefined,
badge: iconUrl('antenna'),
tag: `antenna:${data.body.antenna.id}`,
data,
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
index fac3e707d..5b38743f2 100644
--- a/packages/sw/src/types.ts
+++ b/packages/sw/src/types.ts
@@ -50,4 +50,9 @@ export type BadgeNames =
| 'quote'
| 'repeat'
| 'user-plus'
- | 'users';
+ | 'users'
+ | 'badges'
+ | 'calendar-clock'
+ | 'calendar-check'
+ | 'calendar-exclamation'
+ ;
From 6993e5b7fd1f522c6f06007f08e7991571a0dfc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 16 Jan 2025 23:05:02 +0900
Subject: [PATCH 15/32] =?UTF-8?q?perf(frontend/css):=20MkNote=20=E5=8F=8A?=
=?UTF-8?q?=E3=81=B3=20MkNotification=20=E3=81=AB=20content-visibility=20?=
=?UTF-8?q?=E3=82=92=E9=81=A9=E7=94=A8=20(MisskeyIO#892)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/components/MkNote.vue | 9 ++-------
packages/frontend/src/components/MkNotification.vue | 3 +++
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 54e7e161f..6a67a98a1 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -569,13 +569,8 @@ function emitUpdReaction(emoji: string, delta: number) {
overflow: clip;
contain: content;
- // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
- // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
- // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
- // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
- // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
+ content-visibility: auto;
+ contain-intrinsic-size: auto none auto 128px;
&:focus-visible {
outline: none;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index f40dc1b2b..5bf5188c2 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -218,6 +218,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word;
display: flex;
contain: content;
+
+ content-visibility: auto;
+ contain-intrinsic-size: auto none auto 100px;
}
.head {
From 86209cf4b33d6d1efca81377d11a79258be43d83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 16 Jan 2025 23:29:14 +0900
Subject: [PATCH 16/32] Bump up version to 2024.5.0-io.7 (MisskeyIO#893)
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index f67ad749f..94796d2e3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.5.0-io.6a",
+ "version": "2024.5.0-io.7",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 12f583477..697d0c9ff 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.5.0-io.6a",
+ "version": "2024.5.0-io.7",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
From bcb85f2237ee7383b956f2a012af2b64c492e115 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 04:50:58 +0900
Subject: [PATCH 17/32] fix(backend/object-storage): disable data integrity
protections (MisskeyIO#895)
Cloudflare R2 does not support 'x-amz-checksum-*'
---
packages/backend/src/core/S3Service.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts
index bb2a46335..09f0df1db 100644
--- a/packages/backend/src/core/S3Service.ts
+++ b/packages/backend/src/core/S3Service.ts
@@ -46,6 +46,8 @@ export class S3Service {
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
+ requestChecksumCalculation: 'WHEN_REQUIRED',
+ responseChecksumValidation: 'WHEN_REQUIRED',
});
}
From f69e48944217ab0a9a84c2f9e78fb7a23bff2f6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 04:54:29 +0900
Subject: [PATCH 18/32] Bump up version to 2024.5.0-io.7a (MisskeyIO#896)
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 94796d2e3..46322cfb7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.5.0-io.7",
+ "version": "2024.5.0-io.7a",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 697d0c9ff..38db89f22 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.5.0-io.7",
+ "version": "2024.5.0-io.7a",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
From fc31c0e219be642302b1e488a3b3bf46c90385e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 08:43:17 +0900
Subject: [PATCH 19/32] fix(frontend/MkDraftsDialog): :art: (MisskeyIO#898)
---
.../src/components/MkDraftsDialog.vue | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/packages/frontend/src/components/MkDraftsDialog.vue b/packages/frontend/src/components/MkDraftsDialog.vue
index 289707cd2..6700fd417 100644
--- a/packages/frontend/src/components/MkDraftsDialog.vue
+++ b/packages/frontend/src/components/MkDraftsDialog.vue
@@ -46,15 +46,15 @@
-
+
-
+
-
+
@@ -107,17 +107,17 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -285,8 +285,10 @@ const scheduledPagination = {
}
.draftNoteInfo {
+ display: flex;
flex-shrink: 0;
margin-left: auto;
+ gap: 4px;
}
.draftNoteCw {
From a075b6aeef6c1fb24b9acb766e52ee9f21321ab3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 08:47:46 +0900
Subject: [PATCH 20/32] Bump up version to 2024.5.0-io.7b (MisskeyIO#899)
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 46322cfb7..b7029ddac 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.5.0-io.7a",
+ "version": "2024.5.0-io.7b",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 38db89f22..b9aaa1ad6 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.5.0-io.7a",
+ "version": "2024.5.0-io.7b",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
From f60d9da614d02aeea80fba2c7542d6b1ea600dbf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 16:48:54 +0900
Subject: [PATCH 21/32] =?UTF-8?q?chore(frontend):=20=E9=80=9A=E5=A0=B1?=
=?UTF-8?q?=E3=81=AE=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E3=81=AE=E8=AA=BF?=
=?UTF-8?q?=E6=95=B4=E3=83=BB=E6=8A=95=E7=A8=BF=E3=83=95=E3=82=A9=E3=83=BC?=
=?UTF-8?q?=E3=83=A0=E3=81=AE=E3=82=AC=E3=82=A4=E3=83=89=E3=83=A9=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=B8=E3=81=AE=E5=B0=8E=E7=B7=9A=E3=81=AB=E5=88=A9?=
=?UTF-8?q?=E7=94=A8=E8=A6=8F=E7=B4=84=E3=82=82=E8=BF=BD=E5=8A=A0=20(Missk?=
=?UTF-8?q?eyIO#902)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
locales/en-US.yml | 7 ++++---
locales/index.d.ts | 11 ++++++-----
locales/ja-JP.yml | 8 ++++----
locales/ko-KR.yml | 7 ++++---
packages/frontend/src/components/MkPostForm.vue | 10 +++-------
5 files changed, 21 insertions(+), 22 deletions(-)
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 3ec46cc61..00f348ceb 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"
@@ -2324,6 +2324,7 @@ _postForm:
d: "What do you want to say?"
e: "Start writing..."
f: "Waiting for you to write..."
+ 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 +2349,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 707e058e8..757baea22 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;
@@ -9068,9 +9068,9 @@ export interface Locale extends ILocale {
"f": string;
};
/**
- * [NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。
+ * 投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。
*/
- "guidelineInfo": ParameterizedString<"nsfwGuideUrl">;
+ "tosAndGuidelinesInfo": ParameterizedString<"tosUrl">;
};
"_profile": {
/**
@@ -9166,7 +9166,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 5612736be..f53803b3a 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: "その他"
@@ -2377,7 +2377,7 @@ _postForm:
d: "言いたいことは?"
e: "ここに書いてください"
f: "あなたが書くのを待っています..."
- guidelineInfo: "[NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。"
+ tosAndGuidelinesInfo: "投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。"
_profile:
name: "名前"
@@ -2403,7 +2403,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 d90bd9adf..d51745adb 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: "기타"
@@ -2310,6 +2310,7 @@ _postForm:
d: "말하고 싶은 게 있나요?"
e: "여기에 적어 주세요"
f: "글 쓰기를 기다려요…"
+ tosAndGuidelinesInfo: "노트를 게시하기 전에 [이용약관]({tosUrl})과 [NSFW 가이드라인](https://go.misskey.io/media-guideline)을 반드시 읽어 주세요."
_profile:
name: "이름"
username: "사용자 이름"
@@ -2334,7 +2335,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/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index ddf4a157d..878958da5 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -86,7 +86,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
-
+
+
+
@@ -228,8 +230,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}` : '';
@@ -1443,10 +1443,6 @@ defineExpose({
color: var(--accent);
}
-.guidelineInfo {
- margin-top: 8px;
-}
-
@container (max-width: 500px) {
.headerRight {
font-size: .9em;
From 71c2921b9d940adbd0a3532a1e36a1e8a6a7b682 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 16:49:16 +0900
Subject: [PATCH 22/32] =?UTF-8?q?fix(backend/SigninApiService):=20?=
=?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=AE=E3=83=AC=E3=83=BC?=
=?UTF-8?q?=E3=83=88=E3=83=AA=E3=83=9F=E3=83=83=E3=83=88=E6=99=82=E3=80=81?=
=?UTF-8?q?=E6=84=8F=E5=9B=B3=E3=81=97=E3=81=9F=E3=82=82=E3=81=AE=E3=81=A8?=
=?UTF-8?q?=E9=81=95=E3=81=86=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=A1=E3=83=83?=
=?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95?=
=?UTF-8?q?=E3=82=8C=E3=82=8B=E5=8F=AF=E8=83=BD=E6=80=A7=E3=81=8C=E3=81=82?=
=?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(Missk?=
=?UTF-8?q?eyIO#903)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/backend/src/server/api/SigninApiService.ts | 11 ++++++++---
packages/frontend/src/os.ts | 2 +-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index a56a2d85b..5f264bb73 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/frontend/src/os.ts b/packages/frontend/src/os.ts
index 9a5243926..5a1071a92 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}`;
}
From 655ed957bc2bd55add54cea1b7eb3572ef4d5234 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 16:49:42 +0900
Subject: [PATCH 23/32] =?UTF-8?q?enhance(ux/frontend/MkPostForm):=20?=
=?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF=E3=81=AE=E6=99=82=E9=96=93?=
=?UTF-8?q?=E3=82=92=E5=86=8D=E5=BA=A6=E8=A8=AD=E5=AE=9A=E3=81=99=E3=82=8B?=
=?UTF-8?q?=E6=99=82=E3=80=81=E4=BB=A5=E5=89=8D=E8=A8=AD=E5=AE=9A=E3=81=97?=
=?UTF-8?q?=E3=81=9F=E6=99=82=E9=96=93=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95?=
=?UTF-8?q?=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#904)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/components/MkPostForm.vue | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 878958da5..8ec021d87 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -597,6 +597,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;
From f0f86f1121bae5a2c66daa17bbad73582d604d33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 16:50:01 +0900
Subject: [PATCH 24/32] =?UTF-8?q?spec(backend/queue/ScheduledNote):=20?=
=?UTF-8?q?=E4=BA=88=E7=B4=84=E6=99=82=E9=96=93=E3=81=8C=E9=81=8E=E3=81=8E?=
=?UTF-8?q?=E3=82=8B=E5=89=8D=E3=81=AB=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?=
=?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#905)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processors/CheckMissingScheduledNoteProcessorService.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/CheckMissingScheduledNoteProcessorService.ts
index 8c441a9cc..c55eaa40f 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) {
From 8821e3e81bd859c23457614be21d9ae1c1c6ce6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 17:08:13 +0900
Subject: [PATCH 25/32] =?UTF-8?q?spec(role/ScheduledNote):=20=E3=83=AD?=
=?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=A7=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?=
=?UTF-8?q?=E3=81=AE=E5=80=8B=E6=95=B0=E3=83=BB=E4=BA=88=E7=B4=84=E3=81=AE?=
=?UTF-8?q?=E6=9C=80=E5=A4=A7=E6=97=A5=E6=95=B0=E3=82=92=E5=88=B6=E5=BE=A1?=
=?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(Missk?=
=?UTF-8?q?eyIO#906)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
locales/en-US.yml | 3 ++
locales/index.d.ts | 13 +++++++
locales/ja-JP.yml | 3 ++
locales/ko-KR.yml | 3 ++
.../backend/src/core/NoteCreateService.ts | 9 +++++
packages/backend/src/core/RoleService.ts | 6 +++
.../backend/src/models/json-schema/role.ts | 8 ++++
.../src/server/api/endpoints/notes/create.ts | 35 +++++++++--------
.../frontend/src/components/MkPostForm.vue | 35 +++++++++++++----
packages/frontend/src/const.ts | 2 +
.../frontend/src/pages/admin/roles.editor.vue | 38 +++++++++++++++++++
packages/frontend/src/pages/admin/roles.vue | 14 +++++++
packages/misskey-js/src/autogen/types.ts | 2 +
13 files changed, 148 insertions(+), 23 deletions(-)
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 00f348ceb..2117fb056 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -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"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 757baea22..dce1d15b8 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -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)を必ずお読みください。
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index f53803b3a..e5038ead0 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -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:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index d51745adb..6bc34827a 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -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: "이름"
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 71e430c2d..8a43e100d 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 1b8091cc2..60c935887 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 6cbea1708..69b68c414 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/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index c90c9daf9..ddccbdae5 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/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 8ec021d87..baca7f9f5 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -82,8 +82,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
+
+
+
+
+ {{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
+
+
+
+
+
+
+
@@ -219,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);
@@ -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);
}
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 1483b9d0c..e84958a69 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/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index d4d5b5050..f40d1b742 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
+
+ {{ i18n.ts._role._options.scheduleNoteLimit }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.scheduleNoteLimit.value }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+ {{ i18n.ts._role._options.scheduleNoteMaxDays }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.scheduleNoteMaxDays.value }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
{{ i18n.ts._role._options.canInitiateConversation }}
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 132ecbd31..406da663c 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -56,6 +56,20 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.scheduleNoteLimit }}
+ {{ policies.scheduleNoteLimit }}
+
+
+
+
+
+ {{ i18n.ts._role._options.scheduleNoteMaxDays }}
+ {{ policies.scheduleNoteMaxDays }}
+
+
+
+
{{ i18n.ts._role._options.canInitiateConversation }}
{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b0d2d7662..9990683c8 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5078,6 +5078,8 @@ export type components = {
ltlAvailable: boolean;
canPublicNote: boolean;
canScheduleNote: boolean;
+ scheduleNoteLimit: number;
+ scheduleNoteMaxDays: number;
canInitiateConversation: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
From c4fafbdca7076d7be3c34aa77608a4a391c1784d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 18:35:08 +0900
Subject: [PATCH 26/32] =?UTF-8?q?perf(stream):=20=E5=BF=85=E8=A6=81?=
=?UTF-8?q?=E3=81=AE=E3=81=AA=E3=81=84=E6=97=A2=E8=AA=AD=E5=87=A6=E7=90=86?=
=?UTF-8?q?=E3=81=AE=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=8C?=
=?UTF-8?q?=E7=99=BA=E7=94=9F=E3=81=97=E3=81=AA=E3=81=84=E3=82=88=E3=81=86?=
=?UTF-8?q?=E3=81=AB=20(MisskeyIO#907)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/server/api/stream/channels/antenna.ts | 5 ++++-
.../src/server/api/stream/channels/channel.ts | 5 ++++-
.../server/api/stream/channels/global-timeline.ts | 5 ++++-
.../src/server/api/stream/channels/hashtag.ts | 4 +++-
.../src/server/api/stream/channels/home-timeline.ts | 5 ++++-
.../server/api/stream/channels/hybrid-timeline.ts | 5 ++++-
.../src/server/api/stream/channels/local-timeline.ts | 5 ++++-
.../backend/src/server/api/stream/channels/main.ts | 12 ++++++++++--
.../src/server/api/stream/channels/role-timeline.ts | 5 ++++-
.../src/server/api/stream/channels/user-list.ts | 5 ++++-
packages/frontend/src/scripts/use-note-capture.ts | 7 +++++--
11 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index ff05359b5..f47a644de 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 96ada8c97..7ebc2ed56 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 581618740..371449cfd 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 c7384fdf9..0dbd70b99 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 34890854c..7b38cb59c 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 47d534aa1..4103fb5dc 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 e2192cfad..99d94ace9 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 f93f62568..b7f840c6c 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 67e5cd138..b735507e2 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 d9407ac5d..4b8df4368 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/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index 542d8ab52..db21dcba1 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -60,6 +60,8 @@ export function useNoteCapture(props: {
}
case 'pollVoted': {
+ if (note.value.poll == null) return;
+
const choice = body.choice;
const choices = [...note.value.poll.choices];
@@ -84,8 +86,9 @@ export function useNoteCapture(props: {
function capture(withHandler = false): void {
if (connection) {
- // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
- connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
+ if ($i && (note.value?.visibleUserIds?.includes($i.id) ?? note.value?.mentions?.includes($i.id))) {
+ connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
+ }
if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
}
From 428ff56abe33c75c618d7445210ce159db397733 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 18:35:28 +0900
Subject: [PATCH 27/32] Bump up version to 2024.5.0-io.7c (MisskeyIO#908)
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index b7029ddac..bdd052f60 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/misskey-js/package.json b/packages/misskey-js/package.json
index b9aaa1ad6..7add6a115 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.5.0-io.7b",
+ "version": "2024.5.0-io.7c",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
From d423d9bb445962ef06c96e4d74a7e2d6385ba822 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?=
=?UTF-8?q?=E3=81=AB=E3=82=85?=
<17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 17 Jan 2025 20:47:10 +0900
Subject: [PATCH 28/32] =?UTF-8?q?fix(MisskeyIO#907):=20=E3=82=B5=E3=83=96?=
=?UTF-8?q?=E3=82=B9=E3=82=AF=E3=83=A9=E3=82=A4=E3=83=96=E3=82=B3=E3=83=9E?=
=?UTF-8?q?=E3=83=B3=E3=83=89=E3=81=AF=E5=BF=85=E8=A6=81=E3=81=A0=E3=81=A3?=
=?UTF-8?q?=E3=81=9F=20(MisskeyIO#910)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/scripts/use-note-capture.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index db21dcba1..3511e7a93 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -86,9 +86,14 @@ export function useNoteCapture(props: {
function capture(withHandler = false): void {
if (connection) {
+ let command: string;
if ($i && (note.value?.visibleUserIds?.includes($i.id) ?? note.value?.mentions?.includes($i.id))) {
- connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
+ command = document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's';
+ } else {
+ command = 's';
}
+
+ connection.send(command, { id: note.value.id });
if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
}
From 15414e4ce58e447a696d17a0ce12b238cad8bd61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?sugar=F0=9F=8D=AC=F0=9F=8D=AD=F0=9F=8F=B3=EF=B8=8F?=
=?UTF-8?q?=E2=80=8D=E2=9A=A7=EF=B8=8F?=
<168804988+sugar700@users.noreply.github.com>
Date: Fri, 17 Jan 2025 15:21:08 +0100
Subject: [PATCH 29/32] perf: dynamically load audio visualizer (MisskeyIO#912)
audio visualizer weights 475 kB by itself, and audio files are
relatively rare on timelines, so it makes sense to load it only when
it's necessary
---
packages/frontend/src/components/MkMediaAudio.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 770c1107c..76cc89a26 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only