Compare commits
53 commits
cherrypick
...
main
Author | SHA1 | Date | |
---|---|---|---|
598b88b142 | |||
e90e9e016f | |||
125bf6fe93 | |||
32fc7644fc | |||
04c1c0e1e6 | |||
cfba325ae7 | |||
ed302618f2 | |||
12a9dab275 | |||
55a2b82330 | |||
0ef9fb12c4 | |||
0ceb07314b | |||
f0de9000a0 | |||
83c1dc4105 | |||
71606c5507 | |||
8165088886 | |||
140daecf2f | |||
9e4fd869d2 | |||
884caa8c1e | |||
9f4a76a1d9 | |||
44edae531a | |||
6b7e6bedb3 | |||
d35b2dd0bc | |||
2ac89cd654 | |||
93dba136ab | |||
![]() |
15414e4ce5 | ||
![]() |
d423d9bb44 | ||
![]() |
428ff56abe | ||
![]() |
c4fafbdca7 | ||
![]() |
8821e3e81b | ||
![]() |
f0f86f1121 | ||
![]() |
655ed957bc | ||
![]() |
71c2921b9d | ||
![]() |
f60d9da614 | ||
![]() |
a075b6aeef | ||
![]() |
fc31c0e219 | ||
![]() |
f69e489442 | ||
![]() |
bcb85f2237 | ||
![]() |
86209cf4b3 | ||
![]() |
6993e5b7fd | ||
![]() |
cbe80fdd26 | ||
![]() |
509f385402 | ||
![]() |
68c3eb5b5d | ||
![]() |
5f6cbe048a | ||
![]() |
9792ea0223 | ||
![]() |
cf1952ac0d | ||
![]() |
d4baf040cc | ||
![]() |
c88478f31b | ||
![]() |
f280ce2a39 | ||
![]() |
31d57f270c | ||
![]() |
8a0b98aa26 | ||
![]() |
535a6bc756 | ||
![]() |
8bd7884873 | ||
![]() |
0d24c8843c |
134 changed files with 5304 additions and 2759 deletions
|
@ -175,6 +175,8 @@ flagAsBot: "Mark this account as a bot"
|
||||||
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
|
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
|
||||||
flagAsCat: "Mark this account as a cat"
|
flagAsCat: "Mark this account as a cat"
|
||||||
flagAsCatDescription: "Enable this option to mark this account as a cat."
|
flagAsCatDescription: "Enable this option to mark this account as a cat."
|
||||||
|
flagSpeakAsCat: "Speak as a cat"
|
||||||
|
flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode. If this isn't working, then please check that you dont have 'Disable cat speak' on under General/Note Display"
|
||||||
flagShowTimelineReplies: "Show replies in timeline"
|
flagShowTimelineReplies: "Show replies in timeline"
|
||||||
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
|
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
|
||||||
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
|
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
|
||||||
|
@ -762,6 +764,7 @@ noCrawleDescription: "Ask search engines to not index your profile page, notes,
|
||||||
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
|
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
|
||||||
alwaysMarkSensitive: "Mark as sensitive by default"
|
alwaysMarkSensitive: "Mark as sensitive by default"
|
||||||
loadRawImages: "Load original images instead of showing thumbnails"
|
loadRawImages: "Load original images instead of showing thumbnails"
|
||||||
|
disableCatSpeak: "Disable cat speak"
|
||||||
disableShowingAnimatedImages: "Don't play animated images"
|
disableShowingAnimatedImages: "Don't play animated images"
|
||||||
highlightSensitiveMedia: "Highlight sensitive media"
|
highlightSensitiveMedia: "Highlight sensitive media"
|
||||||
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
||||||
|
@ -1333,6 +1336,14 @@ consentAll: "Allow All Items"
|
||||||
consentSelected: "Allow Selected Items"
|
consentSelected: "Allow Selected Items"
|
||||||
emailAddressLogin: "Login with email address"
|
emailAddressLogin: "Login with email address"
|
||||||
usernameLogin: "Login with username"
|
usernameLogin: "Login with username"
|
||||||
|
autoloadDrafts: "Automatically load drafts when opening the posting form"
|
||||||
|
drafts: "Drafts"
|
||||||
|
unsent: "Unsent"
|
||||||
|
schedule: "Schedule"
|
||||||
|
scheduled: "Scheduled"
|
||||||
|
unschedule: "Unschedule"
|
||||||
|
setScheduledTime: "Set scheduled time"
|
||||||
|
willBePostedAt: "Note will be posted at {x}"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
|
@ -1373,8 +1384,8 @@ _abuseReportCategory:
|
||||||
otherBreach_description: "Other actions that violate the terms but do not fall under specific categories"
|
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: "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"
|
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: "Rights Infringement (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_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: "Dislike This Person"
|
||||||
notLike_description: "Users or posts that you find unpleasant for personal reasons"
|
notLike_description: "Users or posts that you find unpleasant for personal reasons"
|
||||||
other: "Other"
|
other: "Other"
|
||||||
|
@ -1801,6 +1812,9 @@ _role:
|
||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
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"
|
canInitiateConversation: "Can mention, reply or quote"
|
||||||
canCreateContent: "Can create contents"
|
canCreateContent: "Can create contents"
|
||||||
canUpdateContent: "Can edit contents"
|
canUpdateContent: "Can edit contents"
|
||||||
|
@ -2229,6 +2243,7 @@ _permissions:
|
||||||
"read:clip-favorite": "View favorited clips"
|
"read:clip-favorite": "View favorited clips"
|
||||||
"read:federation": "Get federation data"
|
"read:federation": "Get federation data"
|
||||||
"write:report-abuse": "Report violation"
|
"write:report-abuse": "Report violation"
|
||||||
|
"write:push-notification": "Receive push notifications"
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "Granting application permissions"
|
shareAccessTitle: "Granting application permissions"
|
||||||
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
||||||
|
@ -2337,6 +2352,8 @@ _postForm:
|
||||||
d: "What do you want to say?"
|
d: "What do you want to say?"
|
||||||
e: "Start writing..."
|
e: "Start writing..."
|
||||||
f: "Waiting for you to write..."
|
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:
|
_profile:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
username: "Username"
|
username: "Username"
|
||||||
|
@ -2361,7 +2378,7 @@ _profile:
|
||||||
sectionName: "Section name"
|
sectionName: "Section name"
|
||||||
sectionNameNoneDescription: "Do not display the section name"
|
sectionNameNoneDescription: "Do not display the section name"
|
||||||
sectionNameNone: "Section without 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:
|
_exportOrImport:
|
||||||
allNotes: "All notes"
|
allNotes: "All notes"
|
||||||
favoritedNotes: "Favorite notes"
|
favoritedNotes: "Favorite notes"
|
||||||
|
@ -2494,6 +2511,9 @@ _notification:
|
||||||
roleAssigned: "Role given"
|
roleAssigned: "Role given"
|
||||||
emptyPushNotificationMessage: "Push notifications have been updated"
|
emptyPushNotificationMessage: "Push notifications have been updated"
|
||||||
achievementEarned: "Achievement unlocked"
|
achievementEarned: "Achievement unlocked"
|
||||||
|
noteScheduled: "Note has been scheduled"
|
||||||
|
scheduledNotePosted: "Scheduled note has been posted"
|
||||||
|
scheduledNoteError: "Scheduled note has problem with posting"
|
||||||
testNotification: "Test notification"
|
testNotification: "Test notification"
|
||||||
checkNotificationBehavior: "Check notification appearance"
|
checkNotificationBehavior: "Check notification appearance"
|
||||||
sendTestNotification: "Send test notification"
|
sendTestNotification: "Send test notification"
|
||||||
|
@ -2517,6 +2537,9 @@ _notification:
|
||||||
followRequestAccepted: "Accepted follow requests"
|
followRequestAccepted: "Accepted follow requests"
|
||||||
roleAssigned: "Role given"
|
roleAssigned: "Role given"
|
||||||
achievementEarned: "Achievement unlocked"
|
achievementEarned: "Achievement unlocked"
|
||||||
|
noteScheduled: "Note scheduled"
|
||||||
|
scheduledNotePosted: "Scheduled note posted"
|
||||||
|
scheduledNoteError: "Problem with scheduled note"
|
||||||
app: "Notifications from linked apps"
|
app: "Notifications from linked apps"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "followed you back"
|
followBack: "followed you back"
|
||||||
|
|
96
locales/index.d.ts
vendored
96
locales/index.d.ts
vendored
|
@ -724,6 +724,14 @@ export interface Locale extends ILocale {
|
||||||
* にゃにゃにゃ??
|
* にゃにゃにゃ??
|
||||||
*/
|
*/
|
||||||
"flagAsCatDescription": string;
|
"flagAsCatDescription": string;
|
||||||
|
/**
|
||||||
|
* 猫語で話す
|
||||||
|
*/
|
||||||
|
"flagSpeakAsCat": string;
|
||||||
|
/**
|
||||||
|
* 有効にすると、あなたの投稿の 「な」を「にゃ」にします。
|
||||||
|
*/
|
||||||
|
"flagSpeakAsCatDescription": string;
|
||||||
/**
|
/**
|
||||||
* タイムラインにノートへの返信を表示する
|
* タイムラインにノートへの返信を表示する
|
||||||
*/
|
*/
|
||||||
|
@ -3064,6 +3072,10 @@ export interface Locale extends ILocale {
|
||||||
* 添付画像のサムネイルをオリジナル画質にする
|
* 添付画像のサムネイルをオリジナル画質にする
|
||||||
*/
|
*/
|
||||||
"loadRawImages": string;
|
"loadRawImages": string;
|
||||||
|
/**
|
||||||
|
* 猫の話し方を無効にする
|
||||||
|
*/
|
||||||
|
"disableCatSpeak": string;
|
||||||
/**
|
/**
|
||||||
* アニメーション画像を再生しない
|
* アニメーション画像を再生しない
|
||||||
*/
|
*/
|
||||||
|
@ -5386,6 +5398,38 @@ export interface Locale extends ILocale {
|
||||||
* ユーザー名でログイン
|
* ユーザー名でログイン
|
||||||
*/
|
*/
|
||||||
"usernameLogin": string;
|
"usernameLogin": string;
|
||||||
|
/**
|
||||||
|
* 投稿フォームを開いたときに下書きを自動で読み込む
|
||||||
|
*/
|
||||||
|
"autoloadDrafts": string;
|
||||||
|
/**
|
||||||
|
* 下書き
|
||||||
|
*/
|
||||||
|
"drafts": string;
|
||||||
|
/**
|
||||||
|
* 未送信
|
||||||
|
*/
|
||||||
|
"unsent": string;
|
||||||
|
/**
|
||||||
|
* 予約
|
||||||
|
*/
|
||||||
|
"schedule": string;
|
||||||
|
/**
|
||||||
|
* 予約済み
|
||||||
|
*/
|
||||||
|
"scheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約を解除
|
||||||
|
*/
|
||||||
|
"unschedule": string;
|
||||||
|
/**
|
||||||
|
* 予約日時を設定
|
||||||
|
*/
|
||||||
|
"setScheduledTime": string;
|
||||||
|
/**
|
||||||
|
* {x}に投稿されます
|
||||||
|
*/
|
||||||
|
"willBePostedAt": ParameterizedString<"x">;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -5514,11 +5558,11 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"violationRights_description": string;
|
"violationRights_description": string;
|
||||||
/**
|
/**
|
||||||
* 権利侵害やなりすまし(第三者による通報)
|
* 権利侵害(第三者による通報)
|
||||||
*/
|
*/
|
||||||
"violationRightsOther": string;
|
"violationRightsOther": string;
|
||||||
/**
|
/**
|
||||||
* 他人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為
|
* 他人の著作権、商標権、またはその他の権利を侵害する行為
|
||||||
* 第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります
|
* 第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります
|
||||||
*/
|
*/
|
||||||
"violationRightsOther_description": string;
|
"violationRightsOther_description": string;
|
||||||
|
@ -7039,6 +7083,18 @@ export interface Locale extends ILocale {
|
||||||
* パブリック投稿の許可
|
* パブリック投稿の許可
|
||||||
*/
|
*/
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
|
/**
|
||||||
|
* 予約投稿の許可
|
||||||
|
*/
|
||||||
|
"canScheduleNote": string;
|
||||||
|
/**
|
||||||
|
* 予約投稿の最大数
|
||||||
|
*/
|
||||||
|
"scheduleNoteLimit": string;
|
||||||
|
/**
|
||||||
|
* 予約投稿の最大日数
|
||||||
|
*/
|
||||||
|
"scheduleNoteMaxDays": string;
|
||||||
/**
|
/**
|
||||||
* メンション、リプライ、引用の許可
|
* メンション、リプライ、引用の許可
|
||||||
*/
|
*/
|
||||||
|
@ -9110,9 +9166,14 @@ export interface Locale extends ILocale {
|
||||||
"f": string;
|
"f": string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* [NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。
|
* 現在の支援プランで予約できる日数の上限は{max}日です。
|
||||||
|
* [ここ](https://go.misskey.io/donate)からプランをアップグレードできます。
|
||||||
*/
|
*/
|
||||||
"guidelineInfo": ParameterizedString<"nsfwGuideUrl">;
|
"policyScheduleNoteMaxDaysExceeded": ParameterizedString<"max">;
|
||||||
|
/**
|
||||||
|
* 投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。
|
||||||
|
*/
|
||||||
|
"tosAndGuidelinesInfo": ParameterizedString<"tosUrl">;
|
||||||
};
|
};
|
||||||
"_profile": {
|
"_profile": {
|
||||||
/**
|
/**
|
||||||
|
@ -9208,7 +9269,8 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"sectionNameNone": string;
|
"sectionNameNone": string;
|
||||||
/**
|
/**
|
||||||
* 現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。
|
* 現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。
|
||||||
|
* [ここ](https://go.misskey.io/donate)からプランをアップグレードできます。
|
||||||
*/
|
*/
|
||||||
"policyDisplayLimitExceeded": ParameterizedString<"max">;
|
"policyDisplayLimitExceeded": ParameterizedString<"max">;
|
||||||
};
|
};
|
||||||
|
@ -9719,6 +9781,18 @@ export interface Locale extends ILocale {
|
||||||
* 実績を獲得
|
* 実績を獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ノートが予約されました
|
||||||
|
*/
|
||||||
|
"noteScheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿されました
|
||||||
|
*/
|
||||||
|
"scheduledNotePosted": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートを投稿できませんでした
|
||||||
|
*/
|
||||||
|
"scheduledNoteError": string;
|
||||||
/**
|
/**
|
||||||
* 通知テスト
|
* 通知テスト
|
||||||
*/
|
*/
|
||||||
|
@ -9808,6 +9882,18 @@ export interface Locale extends ILocale {
|
||||||
* 実績の獲得
|
* 実績の獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ノートが予約された
|
||||||
|
*/
|
||||||
|
"noteScheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿された
|
||||||
|
*/
|
||||||
|
"scheduledNotePosted": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿できなかった
|
||||||
|
*/
|
||||||
|
"scheduledNoteError": string;
|
||||||
/**
|
/**
|
||||||
* 連携アプリからの通知
|
* 連携アプリからの通知
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -72,7 +72,7 @@ export function build() {
|
||||||
.reduce((a, [k, v]) => (a[k] = (() => {
|
.reduce((a, [k, v]) => (a[k] = (() => {
|
||||||
const [lang] = k.split('-');
|
const [lang] = k.split('-');
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 'ja-JP': return v;
|
case 'ja-JP': return merge(locales['en-US'], v);
|
||||||
case 'ja-KS':
|
case 'ja-KS':
|
||||||
case 'en-US': return merge(locales['ja-JP'], v);
|
case 'en-US': return merge(locales['ja-JP'], v);
|
||||||
default: return merge(
|
default: return merge(
|
||||||
|
|
|
@ -177,6 +177,8 @@ flagAsBot: "Botとして設定"
|
||||||
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
||||||
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
||||||
flagAsCatDescription: "にゃにゃにゃ??"
|
flagAsCatDescription: "にゃにゃにゃ??"
|
||||||
|
flagSpeakAsCat: "猫語で話す"
|
||||||
|
flagSpeakAsCatDescription: "有効にすると、あなたの投稿の 「な」を「にゃ」にします。"
|
||||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
|
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
|
||||||
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
|
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
|
||||||
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
|
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
|
||||||
|
@ -762,6 +764,7 @@ noCrawleDescription: "外部の検索エンジンにあなたのユーザーペ
|
||||||
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
|
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
|
||||||
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
|
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
|
||||||
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
|
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
|
||||||
|
disableCatSpeak: "猫の話し方を無効にする"
|
||||||
disableShowingAnimatedImages: "アニメーション画像を再生しない"
|
disableShowingAnimatedImages: "アニメーション画像を再生しない"
|
||||||
highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
|
highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
|
||||||
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
|
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
|
||||||
|
@ -1340,6 +1343,14 @@ consentAll: "全て許可"
|
||||||
consentSelected: "選択した項目のみ許可"
|
consentSelected: "選択した項目のみ許可"
|
||||||
emailAddressLogin: "メールアドレスでログイン"
|
emailAddressLogin: "メールアドレスでログイン"
|
||||||
usernameLogin: "ユーザー名でログイン"
|
usernameLogin: "ユーザー名でログイン"
|
||||||
|
autoloadDrafts: "投稿フォームを開いたときに下書きを自動で読み込む"
|
||||||
|
drafts: "下書き"
|
||||||
|
unsent: "未送信"
|
||||||
|
schedule: "予約"
|
||||||
|
scheduled: "予約済み"
|
||||||
|
unschedule: "予約を解除"
|
||||||
|
setScheduledTime: "予約日時を設定"
|
||||||
|
willBePostedAt: "{x}に投稿されます"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1376,8 +1387,8 @@ _abuseReportCategory:
|
||||||
otherBreach_description: "明確に分類されないその他の規約違反行為"
|
otherBreach_description: "明確に分類されないその他の規約違反行為"
|
||||||
violationRights: "権利侵害やなりすまし(侵害を受けた権利者本人によるご申告)"
|
violationRights: "権利侵害やなりすまし(侵害を受けた権利者本人によるご申告)"
|
||||||
violationRights_description: "権利者本人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為"
|
violationRights_description: "権利者本人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為"
|
||||||
violationRightsOther: "権利侵害やなりすまし(第三者による通報)"
|
violationRightsOther: "権利侵害(第三者による通報)"
|
||||||
violationRightsOther_description: "他人の著作権、商標権、またはその他の権利を侵害する投稿及びなりすまし行為\n第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります"
|
violationRightsOther_description: "他人の著作権、商標権、またはその他の権利を侵害する行為\n第三者による通報の場合、法律で定められた非親告罪の範囲外のケースには対応できないことがあります"
|
||||||
notLike: "この人が気に入らない"
|
notLike: "この人が気に入らない"
|
||||||
notLike_description: "個人的な理由で不快と感じるユーザーや投稿"
|
notLike_description: "個人的な理由で不快と感じるユーザーや投稿"
|
||||||
other: "その他"
|
other: "その他"
|
||||||
|
@ -1813,6 +1824,9 @@ _role:
|
||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
|
canScheduleNote: "予約投稿の許可"
|
||||||
|
scheduleNoteLimit: "予約投稿の最大数"
|
||||||
|
scheduleNoteMaxDays: "予約投稿の最大日数"
|
||||||
canInitiateConversation: "メンション、リプライ、引用の許可"
|
canInitiateConversation: "メンション、リプライ、引用の許可"
|
||||||
canCreateContent: "コンテンツの作成"
|
canCreateContent: "コンテンツの作成"
|
||||||
canUpdateContent: "コンテンツの編集"
|
canUpdateContent: "コンテンツの編集"
|
||||||
|
@ -2390,7 +2404,8 @@ _postForm:
|
||||||
d: "言いたいことは?"
|
d: "言いたいことは?"
|
||||||
e: "ここに書いてください"
|
e: "ここに書いてください"
|
||||||
f: "あなたが書くのを待っています..."
|
f: "あなたが書くのを待っています..."
|
||||||
guidelineInfo: "[NSFWガイドライン]({nsfwGuideUrl})を必ずお読みになってからご利用ください。"
|
policyScheduleNoteMaxDaysExceeded: "現在の支援プランで予約できる日数の上限は{max}日です。\n[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
|
||||||
|
tosAndGuidelinesInfo: "投稿する前に、[利用規約]({tosUrl})と[NSFWガイドライン](https://go.misskey.io/media-guideline)を必ずお読みください。"
|
||||||
|
|
||||||
_profile:
|
_profile:
|
||||||
name: "名前"
|
name: "名前"
|
||||||
|
@ -2416,7 +2431,7 @@ _profile:
|
||||||
sectionName: "セクション名"
|
sectionName: "セクション名"
|
||||||
sectionNameNoneDescription: "セクション名を表示しないようにする"
|
sectionNameNoneDescription: "セクション名を表示しないようにする"
|
||||||
sectionNameNone: "名前が表示されないセクション"
|
sectionNameNone: "名前が表示されないセクション"
|
||||||
policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
|
policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。\n[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
|
||||||
|
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
|
@ -2558,6 +2573,9 @@ _notification:
|
||||||
roleAssigned: "ロールが付与されました"
|
roleAssigned: "ロールが付与されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
|
noteScheduled: "ノートが予約されました"
|
||||||
|
scheduledNotePosted: "予約済みのノートが投稿されました"
|
||||||
|
scheduledNoteError: "予約済みのノートを投稿できませんでした"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
checkNotificationBehavior: "通知の表示を確かめる"
|
checkNotificationBehavior: "通知の表示を確かめる"
|
||||||
sendTestNotification: "テスト通知を送信する"
|
sendTestNotification: "テスト通知を送信する"
|
||||||
|
@ -2582,6 +2600,9 @@ _notification:
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
roleAssigned: "ロールが付与された"
|
roleAssigned: "ロールが付与された"
|
||||||
achievementEarned: "実績の獲得"
|
achievementEarned: "実績の獲得"
|
||||||
|
noteScheduled: "ノートが予約された"
|
||||||
|
scheduledNotePosted: "予約済みのノートが投稿された"
|
||||||
|
scheduledNoteError: "予約済みのノートが投稿できなかった"
|
||||||
app: "連携アプリからの通知"
|
app: "連携アプリからの通知"
|
||||||
|
|
||||||
_actions:
|
_actions:
|
||||||
|
|
|
@ -1314,6 +1314,14 @@ consentAll: "모두 허용"
|
||||||
consentSelected: "선택한 항목만 허용"
|
consentSelected: "선택한 항목만 허용"
|
||||||
emailAddressLogin: "이메일 주소로 로그인"
|
emailAddressLogin: "이메일 주소로 로그인"
|
||||||
usernameLogin: "사용자명으로 로그인"
|
usernameLogin: "사용자명으로 로그인"
|
||||||
|
autoloadDrafts: "글 작성 시 자동으로 임시 저장된 글 불러오기"
|
||||||
|
drafts: "임시 저장"
|
||||||
|
unsent: "미전송"
|
||||||
|
schedule: "예약"
|
||||||
|
scheduled: "예약됨"
|
||||||
|
unschedule: "예약 취소"
|
||||||
|
setScheduledTime: "예약 시간 설정"
|
||||||
|
willBePostedAt: "{x}에 게시됩니다"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
|
@ -1354,8 +1362,8 @@ _abuseReportCategory:
|
||||||
otherBreach_description: "명확하게 분류되지 않는 기타 규약 위반 행위"
|
otherBreach_description: "명확하게 분류되지 않는 기타 규약 위반 행위"
|
||||||
violationRights: "권리 침해 또는 사칭 (권리자 본인에 의한 신고)"
|
violationRights: "권리 침해 또는 사칭 (권리자 본인에 의한 신고)"
|
||||||
violationRights_description: "권리자 본인의 저작권, 상표권 또는 기타 권리를 침해하는 게시물 및 사칭 행위"
|
violationRights_description: "권리자 본인의 저작권, 상표권 또는 기타 권리를 침해하는 게시물 및 사칭 행위"
|
||||||
violationRightsOther: "권리 침해 또는 사칭 (제3자에 의한 신고)"
|
violationRightsOther: "권리 침해 (제3자에 의한 신고)"
|
||||||
violationRightsOther_description: "타인의 저작권, 상표권 또는 기타 권리를 침해하는 게시물 및 사칭 행위\n제3자에 의한 신고의 경우, 법으로 정해진 비친고죄 범위 외의 사례에는 대응할 수 없습니다"
|
violationRightsOther_description: "타인의 저작권, 상표권 또는 기타 권리를 침해하는 행위\n제3자에 의한 신고의 경우, 법으로 정해진 비친고죄 범위 외의 사례에는 대응할 수 없습니다"
|
||||||
notLike: "이 사람이 마음에 들지 않음"
|
notLike: "이 사람이 마음에 들지 않음"
|
||||||
notLike_description: "개인적인 이유로 불쾌감을 느끼는 사용자나 게시물"
|
notLike_description: "개인적인 이유로 불쾌감을 느끼는 사용자나 게시물"
|
||||||
other: "기타"
|
other: "기타"
|
||||||
|
@ -1782,6 +1790,9 @@ _role:
|
||||||
gtlAvailable: "글로벌 타임라인 보이기"
|
gtlAvailable: "글로벌 타임라인 보이기"
|
||||||
ltlAvailable: "로컬 타임라인 보이기"
|
ltlAvailable: "로컬 타임라인 보이기"
|
||||||
canPublicNote: "공개 노트 허용"
|
canPublicNote: "공개 노트 허용"
|
||||||
|
canScheduleNote: "노트 예약 허용"
|
||||||
|
scheduleNoteLimit: "노트 예약 한도"
|
||||||
|
scheduleNoteMaxDays: "노트 예약 최대 일수"
|
||||||
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
||||||
canCreateContent: "컨텐츠 생성 허용"
|
canCreateContent: "컨텐츠 생성 허용"
|
||||||
canUpdateContent: "컨텐츠 수정 허용"
|
canUpdateContent: "컨텐츠 수정 허용"
|
||||||
|
@ -2301,6 +2312,8 @@ _postForm:
|
||||||
d: "말하고 싶은 게 있나요?"
|
d: "말하고 싶은 게 있나요?"
|
||||||
e: "여기에 적어 주세요"
|
e: "여기에 적어 주세요"
|
||||||
f: "글 쓰기를 기다려요…"
|
f: "글 쓰기를 기다려요…"
|
||||||
|
policyScheduleNoteMaxDaysExceeded: "현재 지원 플랜의 예약 가능한 최대 일수는 {max}일입니다.\n[여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
|
||||||
|
tosAndGuidelinesInfo: "노트를 게시하기 전에 [이용약관]({tosUrl})과 [NSFW 가이드라인](https://go.misskey.io/media-guideline)을 반드시 읽어 주세요."
|
||||||
_profile:
|
_profile:
|
||||||
name: "이름"
|
name: "이름"
|
||||||
username: "사용자 이름"
|
username: "사용자 이름"
|
||||||
|
@ -2325,7 +2338,7 @@ _profile:
|
||||||
sectionName: "섹션 이름"
|
sectionName: "섹션 이름"
|
||||||
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다"
|
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다"
|
||||||
sectionNameNone: "이름이 표시되지 않는 섹션"
|
sectionNameNone: "이름이 표시되지 않는 섹션"
|
||||||
policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다. [여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
|
policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다.\n[여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "모든 노트"
|
allNotes: "모든 노트"
|
||||||
favoritedNotes: "즐겨찾기한 노트"
|
favoritedNotes: "즐겨찾기한 노트"
|
||||||
|
@ -2454,9 +2467,12 @@ _notification:
|
||||||
pollEnded: "투표 결과가 발표되었습니다"
|
pollEnded: "투표 결과가 발표되었습니다"
|
||||||
newNote: "새 게시물"
|
newNote: "새 게시물"
|
||||||
unreadAntennaNote: "안테나 {name}"
|
unreadAntennaNote: "안테나 {name}"
|
||||||
roleAssigned: "역할이 부여 되었습니다."
|
roleAssigned: "역할이 부여 되었습니다"
|
||||||
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
||||||
achievementEarned: "도전 과제를 달성했습니다"
|
achievementEarned: "도전 과제를 달성했습니다"
|
||||||
|
noteScheduled: "노트가 예약되었습니다"
|
||||||
|
scheduledNotePosted: "예약된 노트가 게시되었습니다"
|
||||||
|
scheduledNoteError: "예약된 노트를 게시하지 못했습니다"
|
||||||
testNotification: "알림 테스트"
|
testNotification: "알림 테스트"
|
||||||
checkNotificationBehavior: "알림 표시를 체크하기"
|
checkNotificationBehavior: "알림 표시를 체크하기"
|
||||||
sendTestNotification: "테스트 알림 보내기"
|
sendTestNotification: "테스트 알림 보내기"
|
||||||
|
@ -2480,6 +2496,9 @@ _notification:
|
||||||
followRequestAccepted: "팔로우 요청이 승인되었을 때"
|
followRequestAccepted: "팔로우 요청이 승인되었을 때"
|
||||||
roleAssigned: "역할이 부여 됨"
|
roleAssigned: "역할이 부여 됨"
|
||||||
achievementEarned: "도전 과제 획득"
|
achievementEarned: "도전 과제 획득"
|
||||||
|
noteScheduled: "노트가 예약됨"
|
||||||
|
scheduledNotePosted: "예약된 노트가 게시됨"
|
||||||
|
scheduledNoteError: "예약된 노트를 게시하지 못함"
|
||||||
app: "연동된 앱을 통한 알림"
|
app: "연동된 앱을 통한 알림"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "팔로우"
|
followBack: "팔로우"
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "forkey",
|
"name": "forkey",
|
||||||
"version": "2024.5.0-io.5e",
|
"version": "2024.5.0-io.7c",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.woem.men/woem.men/forkey.git"
|
"url": "https://git.woem.men/woem.men/forkey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@9.15.4",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
|
@ -56,26 +56,26 @@
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"tough-cookie": "5.0.0",
|
"tough-cookie": "5.1.0",
|
||||||
"web-streams-polyfill": "4.0.0"
|
"web-streams-polyfill": "4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano": "7.0.6",
|
"cssnano": "7.0.6",
|
||||||
"execa": "9.5.2",
|
"execa": "9.5.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.5.1",
|
||||||
"terser": "5.37.0",
|
"terser": "5.37.0",
|
||||||
"typescript": "5.7.2"
|
"typescript": "5.7.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.7",
|
||||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||||
"@typescript-eslint/parser": "7.10.0",
|
"@typescript-eslint/parser": "7.10.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.17.0",
|
"cypress": "13.17.0",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"start-server-and-test": "2.0.9"
|
"start-server-and-test": "2.0.10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "4.22.0"
|
"@tensorflow/tfjs-core": "4.22.0"
|
||||||
|
|
BIN
packages/backend/assets/tabler-badges/badges.png
Normal file
BIN
packages/backend/assets/tabler-badges/badges.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-check.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-check.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-exclamation.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-exclamation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-time.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-time.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
12
packages/backend/migration/1696386694000-speakAsCat.js
Normal file
12
packages/backend/migration/1696386694000-speakAsCat.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export class SpeakAsCat1696386694000 {
|
||||||
|
name = "SpeakAsCat1696386694000";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "speakAsCat" boolean NOT NULL DEFAULT true`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."speakAsCat" IS 'Whether to speak as a cat if chosen.'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "speakAsCat"`);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1736599563231-MastodonApp.js
Normal file
18
packages/backend/migration/1736599563231-MastodonApp.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export class MastodonOauth1736599563231 {
|
||||||
|
name = 'MastodonOauth1736599563231'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" ADD "website" character varying(128)`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "app"."website" IS 'Application website.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" ADD "mastodonScopes" character varying(64) array`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "app"."mastodonScopes" IS 'Mastodon app scopes, only set for apps created with Mastodon API.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" ADD "redirectUris" character varying(512) array DEFAULT '{}' NOT NULL`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "app"."redirectUris" IS 'Redirect URIs.'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "website"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "mastodonScopes"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "redirectUris"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class NoteUserIdIdIndex1736888704471 {
|
||||||
|
name = 'NoteUserIdIdIndex1736888704471'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_note_userId_id" ON "note" ("userId", "id") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_note_userId_id"`);
|
||||||
|
}
|
||||||
|
}
|
21
packages/backend/migration/1736923279563-ScheduledNote.js
Normal file
21
packages/backend/migration/1736923279563-ScheduledNote.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export class ScheduledNote1736923279563 {
|
||||||
|
name = 'ScheduledNote1736923279563'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "note_scheduled" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "scheduledAt" TIMESTAMP WITH TIME ZONE, "reason" character varying(256), "userId" character varying(32) NOT NULL, "draft" jsonb NOT NULL, CONSTRAINT "PK_14ca8fa67f70dc68ebab8900f4b" PRIMARY KEY ("id")); COMMENT ON COLUMN "note_scheduled"."createdAt" IS 'The created date of the Note.'; COMMENT ON COLUMN "note_scheduled"."scheduledAt" IS 'The scheduled date of the Note.'; COMMENT ON COLUMN "note_scheduled"."userId" IS 'The ID of author.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_7ddf8710a9faee81081592ec35" ON "note_scheduled" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_bbe52891059217fc31e73e84e2" ON "note_scheduled" ("scheduledAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_b148b24837cc7a2707ae1f0975" ON "note_scheduled" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dfeab22d6bbc4799193997553a" ON "note_scheduled" ("userId", "scheduledAt") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_scheduled" ADD CONSTRAINT "FK_b148b24837cc7a2707ae1f0975a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_scheduled" DROP CONSTRAINT "FK_b148b24837cc7a2707ae1f0975a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_dfeab22d6bbc4799193997553a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_b148b24837cc7a2707ae1f0975"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_bbe52891059217fc31e73e84e2"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_7ddf8710a9faee81081592ec35"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "note_scheduled"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,16 +33,16 @@
|
||||||
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-darwin-arm64": "1.10.3",
|
"@swc/core-darwin-arm64": "1.10.7",
|
||||||
"@swc/core-darwin-x64": "1.10.3",
|
"@swc/core-darwin-x64": "1.10.7",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.10.3",
|
"@swc/core-linux-arm-gnueabihf": "1.10.7",
|
||||||
"@swc/core-linux-arm64-gnu": "1.10.3",
|
"@swc/core-linux-arm64-gnu": "1.10.7",
|
||||||
"@swc/core-linux-arm64-musl": "1.10.3",
|
"@swc/core-linux-arm64-musl": "1.10.7",
|
||||||
"@swc/core-linux-x64-gnu": "1.10.3",
|
"@swc/core-linux-x64-gnu": "1.10.7",
|
||||||
"@swc/core-linux-x64-musl": "1.10.3",
|
"@swc/core-linux-x64-musl": "1.10.7",
|
||||||
"@swc/core-win32-arm64-msvc": "1.10.3",
|
"@swc/core-win32-arm64-msvc": "1.10.7",
|
||||||
"@swc/core-win32-ia32-msvc": "1.10.3",
|
"@swc/core-win32-ia32-msvc": "1.10.7",
|
||||||
"@swc/core-win32-x64-msvc": "1.10.3",
|
"@swc/core-win32-x64-msvc": "1.10.7",
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@tensorflow/tfjs-node": "4.22.0",
|
"@tensorflow/tfjs-node": "4.22.0",
|
||||||
"bufferutil": "4.0.9",
|
"bufferutil": "4.0.9",
|
||||||
|
@ -63,22 +63,22 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authenio/samlify-node-xmllint": "2.0.0",
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
||||||
"@aws-sdk/client-s3": "3.717.0",
|
"@aws-sdk/client-s3": "3.729.0",
|
||||||
"@aws-sdk/lib-storage": "3.717.0",
|
"@aws-sdk/lib-storage": "3.729.0",
|
||||||
"@bull-board/api": "6.5.4",
|
"@bull-board/api": "6.6.2",
|
||||||
"@bull-board/fastify": "6.5.4",
|
"@bull-board/fastify": "6.6.2",
|
||||||
"@bull-board/ui": "6.5.4",
|
"@bull-board/ui": "6.6.2",
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@elastic/elasticsearch": "8.17.0",
|
"@elastic/elasticsearch": "8.17.0",
|
||||||
"@fastify/accepts": "5.0.2",
|
"@fastify/accepts": "5.0.2",
|
||||||
"@fastify/cookie": "11.0.1",
|
"@fastify/cookie": "11.0.2",
|
||||||
"@fastify/cors": "10.0.1",
|
"@fastify/cors": "10.0.2",
|
||||||
"@fastify/express": "4.0.1",
|
"@fastify/express": "4.0.2",
|
||||||
"@fastify/formbody": "8.0.1",
|
"@fastify/formbody": "8.0.2",
|
||||||
"@fastify/http-proxy": "11.0.0",
|
"@fastify/http-proxy": "11.0.1",
|
||||||
"@fastify/multipart": "9.0.1",
|
"@fastify/multipart": "9.0.2",
|
||||||
"@fastify/static": "8.0.3",
|
"@fastify/static": "8.0.4",
|
||||||
"@fastify/view": "10.0.1",
|
"@fastify/view": "10.0.2",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||||
"@napi-rs/canvas": "0.1.65",
|
"@napi-rs/canvas": "0.1.65",
|
||||||
|
@ -86,11 +86,11 @@
|
||||||
"@nestjs/core": "10.4.15",
|
"@nestjs/core": "10.4.15",
|
||||||
"@nestjs/testing": "10.4.15",
|
"@nestjs/testing": "10.4.15",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "13.0.0",
|
"@simplewebauthn/server": "13.1.0",
|
||||||
"@sinonjs/fake-timers": "11.3.1",
|
"@sinonjs/fake-timers": "11.3.1",
|
||||||
"@smithy/node-http-handler": "3.3.3",
|
"@smithy/node-http-handler": "4.0.2",
|
||||||
"@swc/cli": "0.5.2",
|
"@swc/cli": "0.6.0",
|
||||||
"@swc/core": "1.10.3",
|
"@swc/core": "1.10.7",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"bullmq": "5.34.5",
|
"bullmq": "5.34.10",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "10.0.3",
|
"cbor": "10.0.3",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
|
@ -110,11 +110,11 @@
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fastify": "5.2.0",
|
"fastify": "5.2.1",
|
||||||
"fastify-http-errors-enhanced": "6.0.0",
|
"fastify-http-errors-enhanced": "6.0.1",
|
||||||
"fastify-raw-body": "5.0.0",
|
"fastify-raw-body": "5.0.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.6.0",
|
"file-type": "20.0.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.1",
|
"form-data": "4.0.1",
|
||||||
"got": "14.4.5",
|
"got": "14.4.5",
|
||||||
|
@ -127,11 +127,11 @@
|
||||||
"is-svg": "5.1.0",
|
"is-svg": "5.1.0",
|
||||||
"jose": "5.9.6",
|
"jose": "5.9.6",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "25.0.1",
|
"jsdom": "26.0.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.3",
|
"jsonld": "8.3.3",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"meilisearch": "0.47.0",
|
"meilisearch": "0.48.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -163,25 +163,24 @@
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.21.4",
|
"re2": "1.21.4",
|
||||||
"redis-lock": "0.1.4",
|
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
"rss-parser": "3.13.0",
|
"rss-parser": "3.13.0",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"samlify": "2.8.11",
|
"samlify": "2.8.11",
|
||||||
"sanitize-html": "2.14.0",
|
"sanitize-html": "2.14.0",
|
||||||
"secure-json-parse": "3.0.1",
|
"secure-json-parse": "3.0.2",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.23.23",
|
"systeminformation": "5.25.11",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typeorm": "0.3.20",
|
"typeorm": "0.3.20",
|
||||||
"typescript": "5.7.2",
|
"typescript": "5.7.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.7",
|
"web-push": "3.6.7",
|
||||||
|
@ -210,7 +209,7 @@
|
||||||
"@types/jsrsasign": "10.5.15",
|
"@types/jsrsasign": "10.5.15",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.7",
|
||||||
"@types/node-forge": "1.3.11",
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/oauth": "0.9.6",
|
"@types/oauth": "0.9.6",
|
||||||
|
|
13
packages/backend/src/@types/redis-lock.d.ts
vendored
13
packages/backend/src/@types/redis-lock.d.ts
vendored
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module 'redis-lock' {
|
|
||||||
import type Redis from 'ioredis';
|
|
||||||
|
|
||||||
type Lock = (lockName: string, timeout?: number, taskToPerform?: () => Promise<void>) => void;
|
|
||||||
function redisLock(client: Redis.Redis, retryDelay: number): Lock;
|
|
||||||
|
|
||||||
export = redisLock;
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { promisify } from 'node:util';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import redisLock from 'redis-lock';
|
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry delay (ms) for lock acquisition
|
|
||||||
*/
|
|
||||||
const retryDelay = 100;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppLockService {
|
|
||||||
private lock: (key: string, timeout?: number, _?: (() => Promise<void>) | undefined) => Promise<() => void>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
) {
|
|
||||||
this.lock = promisify(redisLock(this.redisClient, retryDelay));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get AP Object lock
|
|
||||||
* @param uri AP object ID
|
|
||||||
* @param timeout Lock timeout (ms), The timeout releases previous lock.
|
|
||||||
* @returns Unlock function
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public getApLock(uri: string, timeout = 30 * 1000): Promise<() => void> {
|
|
||||||
return this.lock(`ap-object:${uri}`, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
|
|
||||||
return this.lock(`chart-insert:${lockKey}`, timeout);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
import { AnnouncementService } from './AnnouncementService.js';
|
import { AnnouncementService } from './AnnouncementService.js';
|
||||||
import { AntennaService } from './AntennaService.js';
|
import { AntennaService } from './AntennaService.js';
|
||||||
import { AppLockService } from './AppLockService.js';
|
|
||||||
import { AchievementService } from './AchievementService.js';
|
import { AchievementService } from './AchievementService.js';
|
||||||
import { AvatarDecorationService } from './AvatarDecorationService.js';
|
import { AvatarDecorationService } from './AvatarDecorationService.js';
|
||||||
import { CaptchaService } from './CaptchaService.js';
|
import { CaptchaService } from './CaptchaService.js';
|
||||||
|
@ -105,6 +104,7 @@ import { UserAccountMoveLogEntityService } from './entities/UserAccountMoveLogEn
|
||||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||||
import { NoteEntityService } from './entities/NoteEntityService.js';
|
import { NoteEntityService } from './entities/NoteEntityService.js';
|
||||||
|
import { ScheduledNoteEntityService } from './entities/ScheduledNoteEntityService.js';
|
||||||
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
||||||
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
|
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
|
||||||
import { NotificationEntityService } from './entities/NotificationEntityService.js';
|
import { NotificationEntityService } from './entities/NotificationEntityService.js';
|
||||||
|
@ -150,7 +150,6 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx
|
||||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||||
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
|
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
|
||||||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||||
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
|
|
||||||
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||||
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
|
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
|
||||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||||
|
@ -248,6 +247,7 @@ const $UserAccountMoveLogEntityService: Provider = { provide: 'UserAccountMoveLo
|
||||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||||
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
||||||
|
const $ScheduledNoteEntityService: Provider = { provide: 'ScheduledNoteEntityService', useExisting: ScheduledNoteEntityService };
|
||||||
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
||||||
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
||||||
const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService };
|
const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService };
|
||||||
|
@ -293,7 +293,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
AiService,
|
AiService,
|
||||||
AnnouncementService,
|
AnnouncementService,
|
||||||
AntennaService,
|
AntennaService,
|
||||||
AppLockService,
|
|
||||||
AchievementService,
|
AchievementService,
|
||||||
AvatarDecorationService,
|
AvatarDecorationService,
|
||||||
CaptchaService,
|
CaptchaService,
|
||||||
|
@ -389,6 +388,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
|
ScheduledNoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
NotificationEntityService,
|
NotificationEntityService,
|
||||||
|
@ -433,7 +433,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$AiService,
|
$AiService,
|
||||||
$AnnouncementService,
|
$AnnouncementService,
|
||||||
$AntennaService,
|
$AntennaService,
|
||||||
$AppLockService,
|
|
||||||
$AchievementService,
|
$AchievementService,
|
||||||
$AvatarDecorationService,
|
$AvatarDecorationService,
|
||||||
$CaptchaService,
|
$CaptchaService,
|
||||||
|
@ -529,6 +528,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
|
$ScheduledNoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
$NotificationEntityService,
|
$NotificationEntityService,
|
||||||
|
@ -571,7 +571,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
AiService,
|
AiService,
|
||||||
AnnouncementService,
|
AnnouncementService,
|
||||||
AntennaService,
|
AntennaService,
|
||||||
AppLockService,
|
|
||||||
AchievementService,
|
AchievementService,
|
||||||
AvatarDecorationService,
|
AvatarDecorationService,
|
||||||
CaptchaService,
|
CaptchaService,
|
||||||
|
@ -666,6 +665,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
|
ScheduledNoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
NotificationEntityService,
|
NotificationEntityService,
|
||||||
|
@ -710,7 +710,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$AiService,
|
$AiService,
|
||||||
$AnnouncementService,
|
$AnnouncementService,
|
||||||
$AntennaService,
|
$AntennaService,
|
||||||
$AppLockService,
|
|
||||||
$AchievementService,
|
$AchievementService,
|
||||||
$AvatarDecorationService,
|
$AvatarDecorationService,
|
||||||
$CaptchaService,
|
$CaptchaService,
|
||||||
|
@ -805,6 +804,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
|
$ScheduledNoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
$NotificationEntityService,
|
$NotificationEntityService,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { join } from 'node:path';
|
||||||
import * as stream from 'node:stream/promises';
|
import * as stream from 'node:stream/promises';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { FSWatcher } from 'chokidar';
|
import { FSWatcher } from 'chokidar';
|
||||||
import * as fileType from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import FFmpeg from 'fluent-ffmpeg';
|
import FFmpeg from 'fluent-ffmpeg';
|
||||||
import isSvg from 'is-svg';
|
import isSvg from 'is-svg';
|
||||||
import probeImageSize from 'probe-image-size';
|
import probeImageSize from 'probe-image-size';
|
||||||
|
@ -309,7 +309,7 @@ export class FileInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public fixMime(mime: string | fileType.MimeType): string {
|
public fixMime(mime: string): string {
|
||||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||||
if (mime === 'audio/x-flac') {
|
if (mime === 'audio/x-flac') {
|
||||||
return 'audio/flac';
|
return 'audio/flac';
|
||||||
|
@ -363,7 +363,7 @@ export class FileInfoService {
|
||||||
return TYPE_OCTET_STREAM;
|
return TYPE_OCTET_STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = await fileType.fileTypeFromFile(path);
|
const type = await fileTypeFromFile(path);
|
||||||
|
|
||||||
if (type) {
|
if (type) {
|
||||||
// XMLはSVGかもしれない
|
// XMLはSVGかもしれない
|
||||||
|
|
|
@ -13,16 +13,28 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
|
import type {
|
||||||
|
ChannelFollowingsRepository,
|
||||||
|
ChannelsRepository,
|
||||||
|
FollowingsRepository,
|
||||||
|
InstancesRepository,
|
||||||
|
MiFollowing,
|
||||||
|
NotesRepository,
|
||||||
|
NoteThreadMutingsRepository,
|
||||||
|
ScheduledNotesRepository,
|
||||||
|
UserListMembershipsRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository
|
||||||
|
} from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
import { MiPoll } from '@/models/Poll.js';
|
import { MiPoll } from '@/models/Poll.js';
|
||||||
|
import type { NoteCreateOption, MinimumUser } from '@/types.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
|
@ -118,35 +130,6 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MinimumUser = {
|
|
||||||
id: MiUser['id'];
|
|
||||||
host: MiUser['host'];
|
|
||||||
username: MiUser['username'];
|
|
||||||
uri: MiUser['uri'];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Option = {
|
|
||||||
createdAt?: 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
@ -169,6 +152,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
@ -229,7 +215,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
host: MiUser['host'];
|
host: MiUser['host'];
|
||||||
isBot: MiUser['isBot'];
|
isBot: MiUser['isBot'];
|
||||||
isCat: MiUser['isCat'];
|
isCat: MiUser['isCat'];
|
||||||
}, data: Option, silent = false): Promise<MiNote> {
|
}, data: NoteCreateOption, silent = false): Promise<MiNote | MiScheduledNote> {
|
||||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
// チャンネル外にリプライしたら対象のスコープに合わせる
|
||||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
||||||
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
||||||
|
@ -425,6 +411,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', `Notes including mentions are limited to ${policies.mentionLimit} users.`);
|
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', `Notes including mentions are limited to ${policies.mentionLimit} users.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.scheduledAt) {
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
|
@ -433,10 +420,30 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
);
|
);
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
|
} else {
|
||||||
|
if (!policies.canScheduleNote) {
|
||||||
|
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!);
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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({
|
const insert = new MiNote({
|
||||||
id: this.idService.gen(data.createdAt?.getTime()),
|
id: this.idService.gen(data.createdAt?.getTime()),
|
||||||
createdAt: data.createdAt!,
|
createdAt: data.createdAt!,
|
||||||
|
@ -534,13 +541,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
|
@bindThis
|
||||||
private async postNoteCreated(note: MiNote, user: {
|
private async postNoteCreated(note: MiNote, user: {
|
||||||
id: MiUser['id'];
|
id: MiUser['id'];
|
||||||
username: MiUser['username'];
|
username: MiUser['username'];
|
||||||
host: MiUser['host'];
|
host: MiUser['host'];
|
||||||
isBot: MiUser['isBot'];
|
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();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
this.notesChart.update(note, true);
|
this.notesChart.update(note, true);
|
||||||
|
@ -792,12 +826,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
private isRenote(note: NoteCreateOption): note is NoteCreateOption & { renote: MiNote } {
|
||||||
return note.renote != null;
|
return note.renote != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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[] }
|
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
|
||||||
) {
|
) {
|
||||||
// NOTE: SYNC WITH misc/is-renote.ts
|
// NOTE: SYNC WITH misc/is-renote.ts
|
||||||
|
@ -873,7 +907,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
private async renderNoteOrRenoteActivity(data: NoteCreateOption, note: MiNote) {
|
||||||
if (data.localOnly) return null;
|
if (data.localOnly) return null;
|
||||||
|
|
||||||
const content = this.isRenote(data) && !this.isQuote(data)
|
const content = this.isRenote(data) && !this.isQuote(data)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
|
import type { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -34,6 +35,11 @@ export class QueueService {
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||||
) {
|
) {
|
||||||
|
this.ensureRepeatJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private ensureRepeatJobs() {
|
||||||
this.systemQueue.add('tickCharts', {
|
this.systemQueue.add('tickCharts', {
|
||||||
}, {
|
}, {
|
||||||
repeat: { pattern: '55 * * * *' },
|
repeat: { pattern: '55 * * * *' },
|
||||||
|
@ -69,6 +75,12 @@ export class QueueService {
|
||||||
repeat: { pattern: '*/5 * * * *' },
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('checkMissingScheduledNote', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -376,7 +388,22 @@ export class QueueService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createReportAbuseJob(report: MiAbuseUserReport) {
|
public createReportAbuseJob(report: MiAbuseUserReport) {
|
||||||
return this.dbQueue.add('reportAbuse', report);
|
return this.dbQueue.add('reportAbuse', report, {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
@bindThis
|
||||||
|
|
|
@ -36,6 +36,9 @@ export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
|
canScheduleNote: boolean;
|
||||||
|
scheduleNoteLimit: number;
|
||||||
|
scheduleNoteMaxDays: number;
|
||||||
canInitiateConversation: boolean;
|
canInitiateConversation: boolean;
|
||||||
canCreateContent: boolean;
|
canCreateContent: boolean;
|
||||||
canUpdateContent: boolean;
|
canUpdateContent: boolean;
|
||||||
|
@ -77,6 +80,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
|
canScheduleNote: true,
|
||||||
|
scheduleNoteLimit: 10,
|
||||||
|
scheduleNoteMaxDays: 365,
|
||||||
canInitiateConversation: true,
|
canInitiateConversation: true,
|
||||||
canCreateContent: true,
|
canCreateContent: true,
|
||||||
canUpdateContent: true,
|
canUpdateContent: true,
|
||||||
|
@ -389,6 +395,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', 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)),
|
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
||||||
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
||||||
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -46,6 +46,8 @@ export class S3Service {
|
||||||
tls: meta.objectStorageUseSSL,
|
tls: meta.objectStorageUseSSL,
|
||||||
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
|
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
|
||||||
requestHandler: new NodeHttpHandler(handlerOption),
|
requestHandler: new NodeHttpHandler(handlerOption),
|
||||||
|
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||||
|
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -14,8 +15,8 @@ import { NotePiningService } from '@/core/NotePiningService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { acquireApObjectLock } from '@/misc/distributed-lock.js';
|
||||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -49,6 +50,9 @@ export class ApInboxService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -77,7 +81,6 @@ export class ApInboxService {
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
private noteDeleteService: NoteDeleteService,
|
private noteDeleteService: NoteDeleteService,
|
||||||
private appLockService: AppLockService,
|
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
|
@ -312,7 +315,7 @@ export class ApInboxService {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
if (this.utilityService.isItemListedIn(this.utilityService.extractHost(uri), meta.blockedHosts)) return 'skip: blocked host';
|
if (this.utilityService.isItemListedIn(this.utilityService.extractHost(uri), meta.blockedHosts)) return 'skip: blocked host';
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await acquireApObjectLock(this.redisForTimelines, uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 既に同じURIを持つものが登録されていないかチェック
|
// 既に同じURIを持つものが登録されていないかチェック
|
||||||
|
@ -440,7 +443,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await acquireApObjectLock(this.redisForTimelines, uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exist = await this.apNoteService.fetchNote(note);
|
const exist = await this.apNoteService.fetchNote(note);
|
||||||
|
@ -543,7 +546,7 @@ export class ApInboxService {
|
||||||
private async deleteNote(actor: MiRemoteUser, uri: string): Promise<string> {
|
private async deleteNote(actor: MiRemoteUser, uri: string): Promise<string> {
|
||||||
this.logger.info(`Deleting the Note: ${uri}`);
|
this.logger.info(`Deleting the Note: ${uri}`);
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await acquireApObjectLock(this.redisForTimelines, uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
||||||
|
@ -813,7 +816,7 @@ export class ApInboxService {
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
} else if (additionalCc && isPost(object)) {
|
} else if (additionalCc && isPost(object)) {
|
||||||
const uri = getApId(object);
|
const uri = getApId(object);
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await acquireApObjectLock(this.redisForTimelines, uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exist = await this.apNoteService.fetchNote(object);
|
const exist = await this.apNoteService.fetchNote(object);
|
||||||
|
|
|
@ -518,6 +518,7 @@ export class ApRendererService {
|
||||||
discoverable: user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
speakAsCat: user.speakAsCat,
|
||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -553,6 +553,9 @@ export const CONTEXTS: (string | Context)[] = [
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
|
// Firefish
|
||||||
|
firefish: "https://joinfirefish.org/ns#",
|
||||||
|
speakAsCat: "firefish:speakAsCat",
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
} satisfies Context,
|
} satisfies Context,
|
||||||
|
|
|
@ -5,15 +5,16 @@
|
||||||
|
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, PollsRepository, EmojisRepository } from '@/models/_.js';
|
import type { UsersRepository, PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import { acquireApObjectLock } from '@/misc/distributed-lock.js';
|
||||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
@ -47,6 +48,9 @@ export class ApNoteService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -70,7 +74,6 @@ export class ApNoteService {
|
||||||
private apImageService: ApImageService,
|
private apImageService: ApImageService,
|
||||||
private apQuestionService: ApQuestionService,
|
private apQuestionService: ApQuestionService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private appLockService: AppLockService,
|
|
||||||
private pollService: PollService,
|
private pollService: PollService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
|
@ -349,7 +352,7 @@ export class ApNoteService {
|
||||||
poll,
|
poll,
|
||||||
uri: note.id,
|
uri: note.id,
|
||||||
url: url,
|
url: url,
|
||||||
}, silent);
|
}, silent) as MiNote;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== 'duplicated') {
|
if (err.name !== 'duplicated') {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -379,7 +382,7 @@ export class ApNoteService {
|
||||||
throw new StatusError('blocked host', 451);
|
throw new StatusError('blocked host', 451);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await acquireApObjectLock(this.redisForTimelines, uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
|
|
|
@ -389,6 +389,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
tags,
|
tags,
|
||||||
isBot,
|
isBot,
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
|
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
|
||||||
emojis,
|
emojis,
|
||||||
})) as MiRemoteUser;
|
})) as MiRemoteUser;
|
||||||
|
|
||||||
|
@ -562,12 +563,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
tags,
|
tags,
|
||||||
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
|
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
|
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
|
||||||
isLocked: person.manuallyApprovesFollowers,
|
isLocked: person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}),
|
...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}),
|
||||||
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
const moving = ((): boolean => {
|
const moving = ((): boolean => {
|
||||||
// 移行先がない→ある
|
// 移行先がない→ある
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/active-users.js';
|
import { name, schema } from './entities/active-users.js';
|
||||||
|
@ -28,11 +29,13 @@ export default class ActiveUsersChart extends Chart<typeof schema> { // eslint-d
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/ap-request.js';
|
import { name, schema } from './entities/ap-request.js';
|
||||||
|
@ -22,10 +23,12 @@ export default class ApRequestChart extends Chart<typeof schema> { // eslint-dis
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/drive.js';
|
import { name, schema } from './entities/drive.js';
|
||||||
|
@ -23,10 +24,12 @@ export default class DriveChart extends Chart<typeof schema> { // eslint-disable
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
|
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/federation.js';
|
import { name, schema } from './entities/federation.js';
|
||||||
|
@ -24,6 +25,9 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@ -31,10 +35,9 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private appLockService: AppLockService,
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,13 +5,14 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/instance.js';
|
import { name, schema } from './entities/instance.js';
|
||||||
|
@ -26,6 +27,9 @@ export default class InstanceChart extends Chart<typeof schema> { // eslint-disa
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -39,10 +43,9 @@ export default class InstanceChart extends Chart<typeof schema> { // eslint-disa
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private appLockService: AppLockService,
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { Not, IsNull, DataSource } from 'typeorm';
|
import { Not, IsNull, DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/notes.js';
|
import { name, schema } from './entities/notes.js';
|
||||||
|
@ -24,13 +25,15 @@ export default class NotesChart extends Chart<typeof schema> { // eslint-disable
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { DriveFilesRepository } from '@/models/_.js';
|
import type { DriveFilesRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/per-user-drive.js';
|
import { name, schema } from './entities/per-user-drive.js';
|
||||||
|
@ -25,14 +26,16 @@ export default class PerUserDriveChart extends Chart<typeof schema> { // eslint-
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { Not, IsNull, DataSource } from 'typeorm';
|
import { Not, IsNull, DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { FollowingsRepository } from '@/models/_.js';
|
import type { FollowingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/per-user-following.js';
|
import { name, schema } from './entities/per-user-following.js';
|
||||||
|
@ -25,14 +26,16 @@ export default class PerUserFollowingChart extends Chart<typeof schema> { // esl
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/per-user-notes.js';
|
import { name, schema } from './entities/per-user-notes.js';
|
||||||
|
@ -25,13 +26,15 @@ export default class PerUserNotesChart extends Chart<typeof schema> { // eslint-
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/per-user-pv.js';
|
import { name, schema } from './entities/per-user-pv.js';
|
||||||
|
@ -24,10 +25,12 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/per-user-reactions.js';
|
import { name, schema } from './entities/per-user-reactions.js';
|
||||||
|
@ -25,11 +26,13 @@ export default class PerUserReactionsChart extends Chart<typeof schema> { // esl
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { name, schema } from './entities/test-grouped.js';
|
import { name, schema } from './entities/test-grouped.js';
|
||||||
import type { KVs } from '../core.js';
|
import type { KVs } from '../core.js';
|
||||||
|
@ -24,10 +25,12 @@ export default class TestGroupedChart extends Chart<typeof schema> { // eslint-d
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { name, schema } from './entities/test-intersection.js';
|
import { name, schema } from './entities/test-intersection.js';
|
||||||
import type { KVs } from '../core.js';
|
import type { KVs } from '../core.js';
|
||||||
|
@ -22,10 +23,12 @@ export default class TestIntersectionChart extends Chart<typeof schema> { // esl
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { name, schema } from './entities/test-unique.js';
|
import { name, schema } from './entities/test-unique.js';
|
||||||
import type { KVs } from '../core.js';
|
import type { KVs } from '../core.js';
|
||||||
|
@ -22,10 +23,12 @@ export default class TestUniqueChart extends Chart<typeof schema> { // eslint-di
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { name, schema } from './entities/test.js';
|
import { name, schema } from './entities/test.js';
|
||||||
import type { KVs } from '../core.js';
|
import type { KVs } from '../core.js';
|
||||||
|
@ -24,10 +25,12 @@ export default class TestChart extends Chart<typeof schema> { // eslint-disable-
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { Not, IsNull, DataSource } from 'typeorm';
|
import { Not, IsNull, DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
import { name, schema } from './entities/users.js';
|
import { name, schema } from './entities/users.js';
|
||||||
|
@ -25,14 +26,16 @@ export default class UsersChart extends Chart<typeof schema> { // eslint-disable
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
private appLockService: AppLockService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
|
|
|
@ -20,14 +20,16 @@ import { RoleEntityService } from './RoleEntityService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.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()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
private noteEntityService: NoteEntityService;
|
private noteEntityService: NoteEntityService;
|
||||||
private roleEntityService: RoleEntityService;
|
private roleEntityService: RoleEntityService;
|
||||||
|
private scheduledNoteEntityService: ScheduledNoteEntityService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -52,6 +54,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
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 the note has been deleted, don't show this notification
|
||||||
if (needsNote && !noteIfNeed) return null;
|
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 needsUser = 'notifierId' in notification;
|
||||||
const userIfNeed = needsUser ? (
|
const userIfNeed = needsUser ? (
|
||||||
hint?.packedUsers != null
|
hint?.packedUsers != null
|
||||||
|
@ -116,6 +124,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
note: noteIfNeed,
|
note: noteIfNeed,
|
||||||
|
draft: draftIfNeed,
|
||||||
reactions,
|
reactions,
|
||||||
});
|
});
|
||||||
} else if (notification.type === 'renote:grouped') {
|
} else if (notification.type === 'renote:grouped') {
|
||||||
|
@ -139,6 +148,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
note: noteIfNeed,
|
note: noteIfNeed,
|
||||||
|
draft: draftIfNeed,
|
||||||
users,
|
users,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -158,6 +168,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
||||||
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
|
...(draftIfNeed != null ? { draft: draftIfNeed } : {}),
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
|
@ -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<Packed<'NoteDraft'>> {
|
||||||
|
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<Packed<'NoteDraft'>[]> {
|
||||||
|
return (await Promise.allSettled(drafts.map(x => this.pack(x, me))))
|
||||||
|
.filter(result => result.status === 'fulfilled')
|
||||||
|
.map(result => (result as PromiseFulfilledResult<Packed<'NoteDraft'>>).value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -489,6 +489,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}))) : [],
|
}))) : [],
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
speakAsCat: user.speakAsCat,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const DI = {
|
||||||
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
||||||
appsRepository: Symbol('appsRepository'),
|
appsRepository: Symbol('appsRepository'),
|
||||||
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
|
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
|
||||||
|
scheduledNotesRepository: Symbol('scheduledNotesRepository'),
|
||||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||||
|
|
44
packages/backend/src/misc/distributed-lock.ts
Normal file
44
packages/backend/src/misc/distributed-lock.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
|
||||||
|
export async function acquireDistributedLock(
|
||||||
|
redis: Redis.Redis,
|
||||||
|
name: string,
|
||||||
|
timeout: number,
|
||||||
|
maxRetries: number,
|
||||||
|
retryInterval: number,
|
||||||
|
): Promise<() => Promise<void>> {
|
||||||
|
const lockKey = `lock:${name}`;
|
||||||
|
const identifier = Math.random().toString(36).slice(2);
|
||||||
|
|
||||||
|
let retries = 0;
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
|
||||||
|
if (result === 'OK') {
|
||||||
|
return async () => {
|
||||||
|
const currentIdentifier = await redis.get(lockKey);
|
||||||
|
if (currentIdentifier === identifier) {
|
||||||
|
await redis.del(lockKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to acquire lock ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acquireApObjectLock(
|
||||||
|
redis: Redis.Redis,
|
||||||
|
uri: string,
|
||||||
|
): Promise<() => Promise<void>> {
|
||||||
|
return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 600, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acquireChartInsertLock(
|
||||||
|
redis: Redis.Redis,
|
||||||
|
name: string,
|
||||||
|
): Promise<() => Promise<void>> {
|
||||||
|
return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 120, 500);
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import { packedMutingSchema } from '@/models/json-schema/muting.js';
|
||||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||||
import { packedNoteSchema } from '@/models/json-schema/note.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 { packedNotificationSchema } from '@/models/json-schema/notification.js';
|
||||||
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
||||||
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
||||||
|
@ -77,6 +78,7 @@ export const refs = {
|
||||||
Announcement: packedAnnouncementSchema,
|
Announcement: packedAnnouncementSchema,
|
||||||
App: packedAppSchema,
|
App: packedAppSchema,
|
||||||
Note: packedNoteSchema,
|
Note: packedNoteSchema,
|
||||||
|
NoteDraft: packedNoteDraftSchema,
|
||||||
NoteReaction: packedNoteReactionSchema,
|
NoteReaction: packedNoteReactionSchema,
|
||||||
NoteFavorite: packedNoteFavoriteSchema,
|
NoteFavorite: packedNoteFavoriteSchema,
|
||||||
Notification: packedNotificationSchema,
|
Notification: packedNotificationSchema,
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { permissions } from 'misskey-js';
|
||||||
|
|
||||||
|
const mastodonToMisskeyScopes: Map<string, (typeof permissions)[number][]> = new Map([
|
||||||
|
['profile', ['read:account']],
|
||||||
|
['follow', ['read:following', 'write:following', 'read:blocks', 'write:blocks', 'read:mutes', 'write:mutes']],
|
||||||
|
['push', ['write:push-notification']],
|
||||||
|
['read:accounts', ['read:account']],
|
||||||
|
['read:blocks', ['read:blocks']],
|
||||||
|
['read:bookmarks', ['read:favorites']],
|
||||||
|
['read:favourites', ['read:reactions']],
|
||||||
|
['read:filters', ['read:account']],
|
||||||
|
['read:follows', ['read:following']],
|
||||||
|
['read:lists', ['read:account']],
|
||||||
|
['read:mutes', ['read:mutes']],
|
||||||
|
['read:notifications', ['read:notifications']],
|
||||||
|
['read:search', []],
|
||||||
|
['read:statuses', []],
|
||||||
|
['write:accounts', ['write:account']],
|
||||||
|
['write:blocks', ['write:blocks']],
|
||||||
|
['write:bookmarks', ['write:favorites']],
|
||||||
|
['write:conversations', ['write:notes']],
|
||||||
|
['write:favourites', ['write:reactions']],
|
||||||
|
['write:filters', ['write:account']],
|
||||||
|
['write:follows', ['write:following']],
|
||||||
|
['write:lists', ['write:account']],
|
||||||
|
['write:media', ['read:drive', 'write:drive']],
|
||||||
|
['write:mutes', ['write:mutes']],
|
||||||
|
['write:notifications', ['write:notifications']],
|
||||||
|
['write:reports', ['write:report-abuse']],
|
||||||
|
['write:statuses', ['write:notes']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function setHighLevelScope(scopeName: string) {
|
||||||
|
const granularScopes = Array.from(mastodonToMisskeyScopes)
|
||||||
|
.flatMap(([key, value]) => key.startsWith(scopeName + ':') ? value : []);
|
||||||
|
mastodonToMisskeyScopes.set(scopeName, Array.from(new Set(granularScopes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setHighLevelScope('read');
|
||||||
|
setHighLevelScope('write');
|
||||||
|
|
||||||
|
export { mastodonToMisskeyScopes };
|
|
@ -63,4 +63,22 @@ export class MiApp {
|
||||||
comment: 'The callbackUrl of the App.',
|
comment: 'The callbackUrl of the App.',
|
||||||
})
|
})
|
||||||
public callbackUrl: string | null;
|
public callbackUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
comment: 'Application website.',
|
||||||
|
})
|
||||||
|
public website: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 64, array: true, nullable: true,
|
||||||
|
comment: 'Mastodon app scopes, only set for apps created with Mastodon API.',
|
||||||
|
})
|
||||||
|
public mastodonScopes: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 512, array: true, nullable: true,
|
||||||
|
comment: 'Redirect URIs.',
|
||||||
|
})
|
||||||
|
public redirectUris: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type { MiDriveFile } from './DriveFile.js';
|
||||||
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
|
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
|
||||||
@Index('IDX_NOTE_FILE_IDS', { synchronize: false })
|
@Index('IDX_NOTE_FILE_IDS', { synchronize: false })
|
||||||
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
|
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
|
||||||
|
@Index('IDX_note_userId_id', ['userId', 'id'])
|
||||||
export class MiNote {
|
export class MiNote {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
|
import { MiScheduledNote } from './ScheduledNote.js';
|
||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
import { MiRole } from './Role.js';
|
import { MiRole } from './Role.js';
|
||||||
|
|
||||||
|
@ -77,6 +78,21 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
achievement: 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';
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
MiModerationLog,
|
MiModerationLog,
|
||||||
MiMuting,
|
MiMuting,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
@ -117,6 +118,12 @@ const $avatarDecorationsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $scheduledNotesRepository: Provider = {
|
||||||
|
provide: DI.scheduledNotesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiScheduledNote),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $noteFavoritesRepository: Provider = {
|
const $noteFavoritesRepository: Provider = {
|
||||||
provide: DI.noteFavoritesRepository,
|
provide: DI.noteFavoritesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
|
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
|
||||||
|
@ -517,6 +524,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
$appsRepository,
|
$appsRepository,
|
||||||
$avatarDecorationsRepository,
|
$avatarDecorationsRepository,
|
||||||
|
$scheduledNotesRepository,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
@ -590,6 +598,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
$appsRepository,
|
$appsRepository,
|
||||||
$avatarDecorationsRepository,
|
$avatarDecorationsRepository,
|
||||||
|
$scheduledNotesRepository,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
|
54
packages/backend/src/models/ScheduledNote.ts
Normal file
54
packages/backend/src/models/ScheduledNote.ts
Normal file
|
@ -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<MiScheduledNote>) {
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
(this as any)[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,6 +186,12 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public isCat: boolean;
|
public isCat: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
comment: 'Whether the User speaks in nya.',
|
||||||
|
})
|
||||||
|
public speakAsCat: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is the root.',
|
comment: 'Whether the User is the root.',
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||||
|
@ -108,6 +109,7 @@ export {
|
||||||
MiMuting,
|
MiMuting,
|
||||||
MiRenoteMuting,
|
MiRenoteMuting,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
@ -181,6 +183,7 @@ export type ModerationLogsRepository = Repository<MiModerationLog>;
|
||||||
export type MutingsRepository = Repository<MiMuting>;
|
export type MutingsRepository = Repository<MiMuting>;
|
||||||
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
||||||
export type NotesRepository = Repository<MiNote>;
|
export type NotesRepository = Repository<MiNote>;
|
||||||
|
export type ScheduledNotesRepository = Repository<MiScheduledNote>;
|
||||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
|
export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
|
||||||
export type NoteReactionsRepository = Repository<MiNoteReaction>;
|
export type NoteReactionsRepository = Repository<MiNoteReaction>;
|
||||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
|
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
|
||||||
|
|
179
packages/backend/src/models/json-schema/note-draft.ts
Normal file
179
packages/backend/src/models/json-schema/note-draft.ts
Normal file
|
@ -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;
|
|
@ -296,6 +296,51 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
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',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -180,6 +180,18 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canScheduleNote: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
scheduleNoteLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
scheduleNoteMaxDays: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
canInitiateConversation: {
|
canInitiateConversation: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -115,6 +115,10 @@ export const packedUserLiteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
speakAsCat: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||||
|
@ -162,6 +163,7 @@ export const entities = [
|
||||||
MiRenoteMuting,
|
MiRenoteMuting,
|
||||||
MiBlocking,
|
MiBlocking,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||||
|
@ -36,6 +37,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro
|
||||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
|
import { ScheduledNoteProcessorService } from './processors/ScheduledNoteProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||||
|
@ -52,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
ResyncChartsProcessorService,
|
ResyncChartsProcessorService,
|
||||||
CleanChartsProcessorService,
|
CleanChartsProcessorService,
|
||||||
CheckExpiredMutingsProcessorService,
|
CheckExpiredMutingsProcessorService,
|
||||||
|
CheckMissingScheduledNoteProcessorService,
|
||||||
CleanProcessorService,
|
CleanProcessorService,
|
||||||
DeleteDriveFilesProcessorService,
|
DeleteDriveFilesProcessorService,
|
||||||
ExportCustomEmojisProcessorService,
|
ExportCustomEmojisProcessorService,
|
||||||
|
@ -75,6 +78,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
CleanRemoteFilesProcessorService,
|
CleanRemoteFilesProcessorService,
|
||||||
RelationshipProcessorService,
|
RelationshipProcessorService,
|
||||||
ReportAbuseProcessorService,
|
ReportAbuseProcessorService,
|
||||||
|
ScheduledNoteProcessorService,
|
||||||
WebhookDeliverProcessorService,
|
WebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
|
|
|
@ -35,10 +35,12 @@ import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesP
|
||||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||||
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.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 { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
|
@ -113,11 +115,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
||||||
private relationshipProcessorService: RelationshipProcessorService,
|
private relationshipProcessorService: RelationshipProcessorService,
|
||||||
private reportAbuseProcessorService: ReportAbuseProcessorService,
|
private reportAbuseProcessorService: ReportAbuseProcessorService,
|
||||||
private tickChartsProcessorService: TickChartsProcessorService,
|
|
||||||
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
||||||
|
private scheduledNoteProcessorService: ScheduledNoteProcessorService,
|
||||||
|
private tickChartsProcessorService: TickChartsProcessorService,
|
||||||
private cleanChartsProcessorService: CleanChartsProcessorService,
|
private cleanChartsProcessorService: CleanChartsProcessorService,
|
||||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||||
|
private checkMissingScheduledNoteProcessorService: CheckMissingScheduledNoteProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
|
@ -141,11 +145,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
//#region system
|
//#region system
|
||||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
|
case 'scheduledNote': return this.scheduledNoteProcessorService.process(job);
|
||||||
case 'tickCharts': return this.tickChartsProcessorService.process();
|
case 'tickCharts': return this.tickChartsProcessorService.process();
|
||||||
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
||||||
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
|
case 'checkMissingScheduledNote': return this.checkMissingScheduledNoteProcessorService.process();
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<void> {
|
||||||
|
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 \'10 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ScheduledNoteJobData>): Promise<string> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import type { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
@ -116,6 +117,10 @@ export type EndedPollNotificationJobData = {
|
||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScheduledNoteJobData = {
|
||||||
|
draftId: MiScheduledNote['id'];
|
||||||
|
};
|
||||||
|
|
||||||
export type WebhookDeliverJobData = {
|
export type WebhookDeliverJobData = {
|
||||||
type: string;
|
type: string;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
import multipart from '@fastify/multipart';
|
import multipart from '@fastify/multipart';
|
||||||
import fastifyCookie from '@fastify/cookie';
|
import fastifyCookie from '@fastify/cookie';
|
||||||
|
import fastifyFormbody from '@fastify/formbody';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
|
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
|
||||||
|
@ -63,6 +64,13 @@ export class ApiServerService {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.register(this.createMisskeyServer);
|
||||||
|
fastify.register(this.createMastodonServer);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private createMisskeyServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
for (const endpoint of endpoints) {
|
for (const endpoint of endpoints) {
|
||||||
const ep = {
|
const ep = {
|
||||||
name: endpoint.name,
|
name: endpoint.name,
|
||||||
|
@ -106,32 +114,6 @@ export class ApiServerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
|
|
||||||
name: endpoint.name,
|
|
||||||
method: endpoint.method,
|
|
||||||
meta: endpoint.meta,
|
|
||||||
params: endpoint.params,
|
|
||||||
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
|
|
||||||
});
|
|
||||||
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
|
|
||||||
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
|
|
||||||
for (const { name, endpoints } of groupedMastodonEndpoints) {
|
|
||||||
fastify.all<{
|
|
||||||
Params: { endpoint: string; },
|
|
||||||
Body: Record<string, unknown>,
|
|
||||||
Querystring: Record<string, unknown>,
|
|
||||||
}>('/' + name, async (request, reply) => {
|
|
||||||
const ep = endpoints.get(request.method);
|
|
||||||
if (!ep) {
|
|
||||||
reply.code(405);
|
|
||||||
reply.send();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.apiCallService.handleMastodonRequest(ep, request, reply);
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fastify.post<{
|
fastify.post<{
|
||||||
Body: {
|
Body: {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -212,4 +194,39 @@ export class ApiServerService {
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private createMastodonServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
|
fastify.register(fastifyFormbody);
|
||||||
|
|
||||||
|
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
|
||||||
|
name: endpoint.name,
|
||||||
|
method: endpoint.method,
|
||||||
|
meta: endpoint.meta,
|
||||||
|
params: endpoint.params,
|
||||||
|
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
|
||||||
|
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
|
||||||
|
|
||||||
|
for (const { name, endpoints } of groupedMastodonEndpoints) {
|
||||||
|
fastify.all<{
|
||||||
|
Params: { endpoint: string; },
|
||||||
|
Body: Record<string, unknown>,
|
||||||
|
Querystring: Record<string, unknown>,
|
||||||
|
}>('/' + name, async (request, reply) => {
|
||||||
|
const ep = endpoints.get(request.method);
|
||||||
|
if (!ep) {
|
||||||
|
reply.code(405);
|
||||||
|
reply.send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.apiCallService.handleMastodonRequest(ep, request, reply);
|
||||||
|
return reply;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,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_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.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_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_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
|
@ -686,6 +688,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_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_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_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_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_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 };
|
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
||||||
|
@ -1087,6 +1091,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
|
$notes_scheduled_cancel,
|
||||||
|
$notes_scheduled_list,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
|
@ -1481,6 +1487,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
|
$notes_scheduled_cancel,
|
||||||
|
$notes_scheduled_list,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
|
|
|
@ -2,8 +2,10 @@ import { Module, Provider } from '@nestjs/common';
|
||||||
|
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
||||||
|
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
|
||||||
|
|
||||||
const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default };
|
const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default };
|
||||||
|
const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___apps_v1_post.default };
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -11,9 +13,11 @@ const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
$accounts_lookup_v1_get,
|
$accounts_lookup_v1_get,
|
||||||
|
$apps_v1_post,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$accounts_lookup_v1_get,
|
$accounts_lookup_v1_get,
|
||||||
|
$apps_v1_post,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MastodonEndpointsModule {}
|
export class MastodonEndpointsModule {}
|
||||||
|
|
|
@ -99,10 +99,15 @@ export class SigninApiService {
|
||||||
reply.code(429);
|
reply.code(429);
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
message: 'Too many failed attempts to sign in. Try again later.',
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
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',
|
||||||
},
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,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_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.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_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_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
|
@ -684,6 +686,8 @@ const eps = [
|
||||||
['notes/conversation', ep___notes_conversation],
|
['notes/conversation', ep___notes_conversation],
|
||||||
['notes/create', ep___notes_create],
|
['notes/create', ep___notes_create],
|
||||||
['notes/delete', ep___notes_delete],
|
['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/create', ep___notes_favorites_create],
|
||||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||||
['notes/featured', ep___notes_featured],
|
['notes/featured', ep___notes_featured],
|
||||||
|
|
|
@ -187,6 +187,7 @@ export const paramDef = {
|
||||||
preventAiLearning: { type: 'boolean' },
|
preventAiLearning: { type: 'boolean' },
|
||||||
isBot: { type: 'boolean' },
|
isBot: { type: 'boolean' },
|
||||||
isCat: { type: 'boolean' },
|
isCat: { type: 'boolean' },
|
||||||
|
speakAsCat: { type: 'boolean' },
|
||||||
injectFeaturedNote: { type: 'boolean' },
|
injectFeaturedNote: { type: 'boolean' },
|
||||||
receiveAnnouncementEmail: { type: 'boolean' },
|
receiveAnnouncementEmail: { type: 'boolean' },
|
||||||
alwaysMarkNsfw: { type: 'boolean' },
|
alwaysMarkNsfw: { type: 'boolean' },
|
||||||
|
@ -341,6 +342,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||||
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
||||||
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||||
|
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
@ -148,6 +149,39 @@ export const meta = {
|
||||||
id: '66819f28-9525-389d-4b0a-4974363fbbbf',
|
id: '66819f28-9525-389d-4b0a-4974363fbbbf',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cannotScheduleToPast: {
|
||||||
|
message: 'Cannot schedule to the past.',
|
||||||
|
code: 'CANNOT_SCHEDULE_TO_PAST',
|
||||||
|
id: 'e577d185-8179-4a17-b47f-6093985558e6',
|
||||||
|
},
|
||||||
|
|
||||||
|
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',
|
||||||
|
kind: 'permission',
|
||||||
|
id: '12f1d5d2-f7ec-4d7c-b608-e873f4b20327',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -207,6 +241,7 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
scheduledAt: { type: 'integer', nullable: true },
|
||||||
noCreatedNote: { type: 'boolean', default: false },
|
noCreatedNote: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
|
@ -263,6 +298,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
||||||
const logger = this.loggerService.getLogger('api:notes:create');
|
const logger = this.loggerService.getLogger('api:notes:create');
|
||||||
|
@ -318,7 +354,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let renote: MiNote | null = null;
|
let renote: MiNote | null = null;
|
||||||
if (ps.renoteId != null) {
|
if (ps.renoteId != null) {
|
||||||
// Fetch renote to note
|
// 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) {
|
if (renote == null) {
|
||||||
logger.error('No such renote target.', { renoteId: ps.renoteId });
|
logger.error('No such renote target.', { renoteId: ps.renoteId });
|
||||||
|
@ -371,7 +407,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let reply: MiNote | null = null;
|
let reply: MiNote | null = null;
|
||||||
if (ps.replyId != null) {
|
if (ps.replyId != null) {
|
||||||
// Fetch reply
|
// Fetch reply
|
||||||
reply = await this.notesRepository.findOneBy({ id: ps.replyId });
|
reply = await this.notesRepository.findOne({ where: { id: ps.replyId }, relations: ['user'] });
|
||||||
|
|
||||||
if (reply == null) {
|
if (reply == null) {
|
||||||
logger.error('No such reply target.', { replyId: ps.replyId });
|
logger.error('No such reply target.', { replyId: ps.replyId });
|
||||||
|
@ -384,12 +420,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
} else if ( me.isBot ) {
|
} else if (me.isBot && reply.user!.isBot) {
|
||||||
const replayuser = await this.usersRepository.findOneBy({ id: reply.userId });
|
|
||||||
if (replayuser?.isBot) {
|
|
||||||
throw new ApiError(meta.errors.replyingToAnotherBot);
|
throw new ApiError(meta.errors.replyingToAnotherBot);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (reply.userId !== me.id) {
|
if (reply.userId !== me.id) {
|
||||||
|
@ -427,10 +460,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
try {
|
try {
|
||||||
const note = await this.noteCreateService.create(me, {
|
const note = await this.noteCreateService.create(me, {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
scheduledAt: ps.scheduledAt ? scheduledAt : null,
|
||||||
files: files,
|
files: files,
|
||||||
poll: ps.poll ? {
|
poll: ps.poll ? {
|
||||||
choices: ps.poll.choices,
|
choices: ps.poll.choices,
|
||||||
|
@ -454,10 +500,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// 1分間、リクエストの処理結果を記録
|
// 1分間、リクエストの処理結果を記録
|
||||||
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
||||||
|
|
||||||
|
if (!scheduledAt) {
|
||||||
logger.info('Successfully created a note.', { noteId: note.id });
|
logger.info('Successfully created a note.', { noteId: note.id });
|
||||||
if (ps.noCreatedNote) return;
|
} 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 {
|
else return {
|
||||||
createdNote: await this.noteEntityService.pack(note, me),
|
createdNote: await this.noteEntityService.pack(note as MiNote, me),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除
|
// エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除
|
||||||
|
@ -466,8 +520,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
logger.error('Failed to create a note.', { error: err });
|
logger.error('Failed to create a note.', { error: err });
|
||||||
|
|
||||||
if (err instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
|
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);
|
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;
|
throw err;
|
||||||
|
|
|
@ -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<typeof meta, typeof paramDef> { // 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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import { Schema } from '@/misc/json-schema.js';
|
import { Schema } from '@/misc/json-schema.js';
|
||||||
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
||||||
|
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
|
||||||
|
|
||||||
const eps = [
|
const eps = [
|
||||||
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
|
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
|
||||||
|
['POST', 'v1/apps', mep___apps_v1_post],
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IMastodonEndpointMeta {
|
export interface IMastodonEndpointMeta {
|
||||||
|
|
80
packages/backend/src/server/api/mastodon/apps/v1/post.ts
Normal file
80
packages/backend/src/server/api/mastodon/apps/v1/post.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { MastodonEndpoint } from '@/server/api/mastodon-endpoint-base.js';
|
||||||
|
import { MastodonApiError } from '@/server/api/error.js';
|
||||||
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
import type { AppsRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
client_name: { type: 'string', minLength: 1 },
|
||||||
|
redirect_uri: { type: 'string', minLength: 1 },
|
||||||
|
redirect_uris: {
|
||||||
|
anyOf: [
|
||||||
|
{ type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
|
||||||
|
{ type: 'string', minLength: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
scopes: { type: 'string', minLength: 1 },
|
||||||
|
website: { type: 'string' },
|
||||||
|
},
|
||||||
|
anyOf: [
|
||||||
|
{ required: ['redirect_uri'] },
|
||||||
|
{ required: ['redirect_uris'] },
|
||||||
|
],
|
||||||
|
required: ['client_name'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends MastodonEndpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.appsRepository)
|
||||||
|
private appsRepository: AppsRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const redirectUrlsRawValue = ps.redirect_uris ?? ps.redirect_uri ?? [];
|
||||||
|
const redirectUris = typeof redirectUrlsRawValue === 'string' ? redirectUrlsRawValue.split('\n') : redirectUrlsRawValue;
|
||||||
|
const secret = secureRndstr(32);
|
||||||
|
const mastodonScopes = (ps.scopes ?? 'read').split(' ');
|
||||||
|
const scopes = mastodonScopes.flatMap(scope => {
|
||||||
|
const misskeyScopes = mastodonToMisskeyScopes.get(scope);
|
||||||
|
if (!misskeyScopes) {
|
||||||
|
throw new MastodonApiError('Scopes doesn\'t match configured on the server.', 400);
|
||||||
|
}
|
||||||
|
return misskeyScopes;
|
||||||
|
});
|
||||||
|
const clientId = this.idService.gen();
|
||||||
|
await this.appsRepository.insert({
|
||||||
|
id: clientId,
|
||||||
|
userId: me ? me.id : null,
|
||||||
|
name: ps.client_name,
|
||||||
|
description: ps.website ?? '',
|
||||||
|
permission: scopes,
|
||||||
|
callbackUrl: null,
|
||||||
|
secret: secret,
|
||||||
|
website: ps.website,
|
||||||
|
mastodonScopes: mastodonScopes,
|
||||||
|
redirectUris: redirectUris,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: clientId,
|
||||||
|
name: ps.client_name,
|
||||||
|
website: ps.website,
|
||||||
|
scopes: mastodonScopes,
|
||||||
|
redirect_uri: redirectUris.join('\n'),
|
||||||
|
redirect_uris: redirectUris,
|
||||||
|
client_id: `mastodon:${clientId}`,
|
||||||
|
client_secret: secret,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,10 @@ class AntennaChannel extends Channel {
|
||||||
|
|
||||||
if (this.isNoteMutedOrBlocked(note)) return;
|
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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -59,7 +63,6 @@ class AntennaChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -65,7 +69,6 @@ class ChannelChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -95,7 +99,6 @@ class GlobalTimelineChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,9 @@ class HashtagChannel extends Channel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) {
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
}
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -99,7 +103,6 @@ class HomeTimelineChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -113,7 +117,6 @@ class HybridTimelineChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -98,7 +102,6 @@ class LocalTimelineChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,11 @@ class MainChannel extends Channel {
|
||||||
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) {
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
}
|
||||||
|
|
||||||
data.body.note = note;
|
data.body.note = note;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -51,7 +55,11 @@ class MainChannel extends Channel {
|
||||||
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.user && (note.visibleUserIds?.includes(this.user.id) ?? note.mentions?.includes(this.user.id))) {
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
}
|
||||||
|
|
||||||
data.body = note;
|
data.body = note;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -81,7 +85,6 @@ class RoleTimelineChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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)) {
|
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||||
this.send('note', {
|
this.send('note', {
|
||||||
id: note.id, myReaction: note.myReaction,
|
id: note.id, myReaction: note.myReaction,
|
||||||
|
@ -138,7 +142,6 @@ class UserListChannel extends Channel {
|
||||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.connection.cacheNote(note);
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type {
|
import type {
|
||||||
AccessTokensRepository,
|
AccessTokensRepository,
|
||||||
|
AppsRepository,
|
||||||
IndieAuthClientsRepository,
|
IndieAuthClientsRepository,
|
||||||
UserProfilesRepository,
|
UserProfilesRepository,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
|
@ -45,9 +46,15 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { normalizeEmailAddress } from '@/misc/normalize-email-address.js';
|
import { normalizeEmailAddress } from '@/misc/normalize-email-address.js';
|
||||||
|
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
|
||||||
import type { ServerResponse } from 'node:http';
|
import type { ServerResponse } from 'node:http';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
|
function extractMastodonAppId(clientId: string): string | null {
|
||||||
|
const MASTODON_CLIENT_ID_PREFIX = 'mastodon:';
|
||||||
|
return clientId.startsWith(MASTODON_CLIENT_ID_PREFIX) ? clientId.substring(MASTODON_CLIENT_ID_PREFIX.length) : null;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Consider migrating to @node-oauth/oauth2-server once
|
// TODO: Consider migrating to @node-oauth/oauth2-server once
|
||||||
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
|
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
|
||||||
// Upstream the various validations and RFC9207 implementation in that case.
|
// Upstream the various validations and RFC9207 implementation in that case.
|
||||||
|
@ -258,6 +265,8 @@ export class OAuth2ProviderService {
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
@Inject(DI.appsRepository)
|
||||||
|
private appsRepository: AppsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
@ -328,13 +337,25 @@ export class OAuth2ProviderService {
|
||||||
if (body.client_id !== granted.clientId) return;
|
if (body.client_id !== granted.clientId) return;
|
||||||
if (redirectUri !== granted.redirectUri) return;
|
if (redirectUri !== granted.redirectUri) return;
|
||||||
|
|
||||||
|
const mastodonAppId = extractMastodonAppId(granted.clientId);
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
|
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
|
||||||
|
// For Mastodon API, code verifier isn't necessary (but if code challenge was provided, then it should be verified)
|
||||||
|
if (!mastodonAppId || granted.codeChallenge || body.code_verifier) {
|
||||||
if (!body.code_verifier) return;
|
if (!body.code_verifier) return;
|
||||||
if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
|
if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = secureRndstr(128);
|
const accessToken = secureRndstr(128);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
let scopes = granted.scopes;
|
||||||
|
let name = granted.clientId;
|
||||||
|
if (mastodonAppId) {
|
||||||
|
scopes = [...new Set(granted.scopes.flatMap((scope: string) => mastodonToMisskeyScopes.get(scope)))];
|
||||||
|
name = (await this.appsRepository.findOneBy({ id: mastodonAppId }))?.name ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: we don't have a setup for automatic token expiration
|
// NOTE: we don't have a setup for automatic token expiration
|
||||||
await accessTokensRepository.insert({
|
await accessTokensRepository.insert({
|
||||||
id: idService.gen(now.getTime()),
|
id: idService.gen(now.getTime()),
|
||||||
|
@ -342,8 +363,9 @@ export class OAuth2ProviderService {
|
||||||
userId: granted.userId,
|
userId: granted.userId,
|
||||||
token: accessToken,
|
token: accessToken,
|
||||||
hash: accessToken,
|
hash: accessToken,
|
||||||
name: granted.clientId,
|
name: name,
|
||||||
permission: granted.scopes,
|
permission: scopes,
|
||||||
|
appId: mastodonAppId,
|
||||||
});
|
});
|
||||||
|
|
||||||
grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`);
|
grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`);
|
||||||
|
@ -354,7 +376,7 @@ export class OAuth2ProviderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
granted.grantedToken = accessToken;
|
granted.grantedToken = accessToken;
|
||||||
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
|
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${scopes}]`);
|
||||||
await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5);
|
await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5);
|
||||||
|
|
||||||
return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
|
return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
|
||||||
|
@ -418,6 +440,31 @@ export class OAuth2ProviderService {
|
||||||
|
|
||||||
this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
|
this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
|
||||||
|
|
||||||
|
let clientInfo: ClientInformation;
|
||||||
|
const mastodonAppId = extractMastodonAppId(clientID);
|
||||||
|
if (mastodonAppId) {
|
||||||
|
const app = await this.appsRepository.findOneBy({ id: mastodonAppId });
|
||||||
|
if (!app) {
|
||||||
|
throw new AuthorizationError('unrecognized client id', 'invalid_request');
|
||||||
|
}
|
||||||
|
clientInfo = {
|
||||||
|
id: clientID,
|
||||||
|
name: app.name,
|
||||||
|
redirectUris: app.redirectUris,
|
||||||
|
};
|
||||||
|
if (codeChallengeMethod && codeChallengeMethod !== 'S256') {
|
||||||
|
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const scopes = [...new Set(scope)].filter(s => mastodonToMisskeyScopes.has(s));
|
||||||
|
if (!scopes.length) {
|
||||||
|
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
|
||||||
|
}
|
||||||
|
areq.scope = scopes;
|
||||||
|
} catch (err) {
|
||||||
|
return [err as Error, clientInfo, redirectURI];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const clientUrl = validateClientId(clientID);
|
const clientUrl = validateClientId(clientID);
|
||||||
|
|
||||||
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||||
|
@ -434,13 +481,7 @@ export class OAuth2ProviderService {
|
||||||
// Find client information from the database.
|
// Find client information from the database.
|
||||||
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
|
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
|
||||||
// Find client information from the remote.
|
// Find client information from the remote.
|
||||||
const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
|
clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
|
||||||
|
|
||||||
// Require the redirect URI to be included in an explicit list, per
|
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
|
|
||||||
if (!clientInfo.redirectUris.includes(redirectURI)) {
|
|
||||||
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
|
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
|
||||||
|
@ -449,7 +490,7 @@ export class OAuth2ProviderService {
|
||||||
}
|
}
|
||||||
areq.scope = scopes;
|
areq.scope = scopes;
|
||||||
|
|
||||||
// Require PKCE parameters.
|
// Require PKCE parameters. This requirement is skipped for Mastodon clients, as Mastodon API doesn't require it.
|
||||||
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
|
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
|
||||||
if (typeof codeChallenge !== 'string') {
|
if (typeof codeChallenge !== 'string') {
|
||||||
|
@ -461,6 +502,12 @@ export class OAuth2ProviderService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return [err as Error, clientInfo, redirectURI];
|
return [err as Error, clientInfo, redirectURI];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Require the redirect URI to be included in an explicit list, per
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
|
||||||
|
if (!clientInfo.redirectUris.includes(redirectURI)) {
|
||||||
|
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
return [null, clientInfo, redirectURI];
|
return [null, clientInfo, redirectURI];
|
||||||
})().then(args => done(...args), err => done(err));
|
})().then(args => done(...args), err => done(err));
|
||||||
|
|
|
@ -3,6 +3,13 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 - 通知オンにしているユーザーが投稿した
|
* note - 通知オンにしているユーザーが投稿した
|
||||||
* follow - フォローされた
|
* follow - フォローされた
|
||||||
|
@ -16,6 +23,9 @@
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
* roleAssigned - ロールが付与された
|
* roleAssigned - ロールが付与された
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
|
* noteScheduled - 予約投稿が予約された
|
||||||
|
* scheduledNotePosted - 予約投稿が投稿された
|
||||||
|
* scheduledNoteError - 予約投稿がエラーになった
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
|
@ -32,6 +42,9 @@ export const notificationTypes = [
|
||||||
'followRequestAccepted',
|
'followRequestAccepted',
|
||||||
'roleAssigned',
|
'roleAssigned',
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
|
'noteScheduled',
|
||||||
|
'scheduledNotePosted',
|
||||||
|
'scheduledNoteError',
|
||||||
'app',
|
'app',
|
||||||
'test',
|
'test',
|
||||||
] as const;
|
] as const;
|
||||||
|
@ -350,6 +363,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<T> = {
|
export type Serialized<T> = {
|
||||||
[K in keyof T]:
|
[K in keyof T]:
|
||||||
T[K] extends Date
|
T[K] extends Date
|
||||||
|
|
|
@ -40,6 +40,7 @@ describe('ユーザー', () => {
|
||||||
avatarDecorations: user.avatarDecorations,
|
avatarDecorations: user.avatarDecorations,
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
speakAsCat: user.speakAsCat,
|
||||||
instance: user.instance,
|
instance: user.instance,
|
||||||
emojis: user.emojis,
|
emojis: user.emojis,
|
||||||
onlineStatus: user.onlineStatus,
|
onlineStatus: user.onlineStatus,
|
||||||
|
@ -311,6 +312,7 @@ describe('ユーザー', () => {
|
||||||
assert.deepStrictEqual(response.avatarDecorations, []);
|
assert.deepStrictEqual(response.avatarDecorations, []);
|
||||||
assert.strictEqual(response.isBot, false);
|
assert.strictEqual(response.isBot, false);
|
||||||
assert.strictEqual(response.isCat, false);
|
assert.strictEqual(response.isCat, false);
|
||||||
|
assert.strictEqual(response.speakAsCat, false);
|
||||||
assert.strictEqual(response.instance, undefined);
|
assert.strictEqual(response.instance, undefined);
|
||||||
assert.deepStrictEqual(response.emojis, {});
|
assert.deepStrictEqual(response.emojis, {});
|
||||||
assert.strictEqual(response.onlineStatus, 'unknown');
|
assert.strictEqual(response.onlineStatus, 'unknown');
|
||||||
|
@ -446,6 +448,8 @@ describe('ユーザー', () => {
|
||||||
{ parameters: () => ({ isBot: false }) },
|
{ parameters: () => ({ isBot: false }) },
|
||||||
{ parameters: () => ({ isCat: true }) },
|
{ parameters: () => ({ isCat: true }) },
|
||||||
{ parameters: () => ({ isCat: false }) },
|
{ parameters: () => ({ isCat: false }) },
|
||||||
|
{ parameters: () => ({ speakAsCat: true }) },
|
||||||
|
{ parameters: () => ({ speakAsCat: false }) },
|
||||||
{ parameters: () => ({ injectFeaturedNote: true }) },
|
{ parameters: () => ({ injectFeaturedNote: true }) },
|
||||||
{ parameters: () => ({ injectFeaturedNote: false }) },
|
{ parameters: () => ({ injectFeaturedNote: false }) },
|
||||||
{ parameters: () => ({ receiveAnnouncementEmail: true }) },
|
{ parameters: () => ({ receiveAnnouncementEmail: true }) },
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as assert from 'assert';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import * as lolex from '@sinonjs/fake-timers';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import TestChart from '@/core/chart/charts/test.js';
|
import TestChart from '@/core/chart/charts/test.js';
|
||||||
import TestGroupedChart from '@/core/chart/charts/test-grouped.js';
|
import TestGroupedChart from '@/core/chart/charts/test-grouped.js';
|
||||||
import TestUniqueChart from '@/core/chart/charts/test-unique.js';
|
import TestUniqueChart from '@/core/chart/charts/test-unique.js';
|
||||||
|
@ -18,16 +19,16 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t
|
||||||
import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js';
|
import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js';
|
||||||
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
|
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import type { AppLockService } from '@/core/AppLockService.js';
|
|
||||||
import { coreLogger } from '@/logger.js';
|
import { coreLogger } from '@/logger.js';
|
||||||
|
|
||||||
describe('Chart', () => {
|
describe('Chart', () => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const appLockService = {
|
|
||||||
getChartInsertLock: () => () => Promise.resolve(() => {}),
|
|
||||||
} as unknown as jest.Mocked<AppLockService>;
|
|
||||||
|
|
||||||
let db: DataSource | undefined;
|
let db: DataSource | undefined;
|
||||||
|
let redisForTimelines = {
|
||||||
|
set: () => Promise.resolve('OK'),
|
||||||
|
get: () => Promise.resolve(null),
|
||||||
|
} as unknown as jest.Mocked<Redis.Redis>;
|
||||||
|
|
||||||
let testChart: TestChart;
|
let testChart: TestChart;
|
||||||
let testGroupedChart: TestGroupedChart;
|
let testGroupedChart: TestGroupedChart;
|
||||||
|
@ -64,10 +65,10 @@ describe('Chart', () => {
|
||||||
await db.initialize();
|
await db.initialize();
|
||||||
|
|
||||||
const logger = coreLogger.createSubLogger('chart'); // TODO: モックにする
|
const logger = coreLogger.createSubLogger('chart'); // TODO: モックにする
|
||||||
testChart = new TestChart(db, appLockService, logger);
|
testChart = new TestChart(db, redisForTimelines, logger);
|
||||||
testGroupedChart = new TestGroupedChart(db, appLockService, logger);
|
testGroupedChart = new TestGroupedChart(db, redisForTimelines, logger);
|
||||||
testUniqueChart = new TestUniqueChart(db, appLockService, logger);
|
testUniqueChart = new TestUniqueChart(db, redisForTimelines, logger);
|
||||||
testIntersectionChart = new TestIntersectionChart(db, appLockService, logger);
|
testIntersectionChart = new TestIntersectionChart(db, redisForTimelines, logger);
|
||||||
|
|
||||||
clock = lolex.install({
|
clock = lolex.install({
|
||||||
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||||
|
|
|
@ -100,6 +100,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||||
isBlocking: false,
|
isBlocking: false,
|
||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: false,
|
isCat: false,
|
||||||
|
speakAsCat: false,
|
||||||
isFollowed: false,
|
isFollowed: false,
|
||||||
isFollowing: false,
|
isFollowing: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
|
|
|
@ -27,11 +27,11 @@
|
||||||
"@rollup/plugin-typescript": "12.1.2",
|
"@rollup/plugin-typescript": "12.1.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@syuilo/aiscript": "0.19.0",
|
"@syuilo/aiscript": "0.19.0",
|
||||||
"@tabler/icons-webfont": "3.26.0",
|
"@tabler/icons-webfont": "3.28.1",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.2.1",
|
"@vitejs/plugin-vue": "5.2.1",
|
||||||
"@vue/compiler-sfc": "3.5.13",
|
"@vue/compiler-sfc": "3.5.13",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.13",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.2.0",
|
"chartjs-plugin-zoom": "2.2.0",
|
||||||
"chromatic": "11.20.2",
|
"chromatic": "11.24.0",
|
||||||
"compare-versions": "6.1.1",
|
"compare-versions": "6.1.1",
|
||||||
"cropperjs": "2.0.0-rc.0",
|
"cropperjs": "2.0.0-rc.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
|
@ -59,21 +59,21 @@
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.29.1",
|
"rollup": "4.30.1",
|
||||||
"sanitize-html": "2.14.0",
|
"sanitize-html": "2.14.0",
|
||||||
"sass": "1.83.0",
|
"sass": "1.83.4",
|
||||||
"shiki": "1.24.4",
|
"shiki": "1.27.2",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.171.0",
|
"three": "0.172.0",
|
||||||
"throttle-debounce": "5.0.2",
|
"throttle-debounce": "5.0.2",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.7.2",
|
"typescript": "5.7.3",
|
||||||
"uuid": "11.0.3",
|
"uuid": "11.0.5",
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "6.0.6",
|
"vite": "6.0.7",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-gtag": "2.0.1",
|
"vue-gtag": "2.0.1",
|
||||||
"vuedraggable": "next",
|
"vuedraggable": "next",
|
||||||
|
@ -82,33 +82,33 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||||
"@storybook/addon-actions": "8.4.7",
|
"@storybook/addon-actions": "8.5.0",
|
||||||
"@storybook/addon-essentials": "8.4.7",
|
"@storybook/addon-essentials": "8.5.0",
|
||||||
"@storybook/addon-interactions": "8.4.7",
|
"@storybook/addon-interactions": "8.5.0",
|
||||||
"@storybook/addon-links": "8.4.7",
|
"@storybook/addon-links": "8.5.0",
|
||||||
"@storybook/addon-mdx-gfm": "8.4.7",
|
"@storybook/addon-mdx-gfm": "8.5.0",
|
||||||
"@storybook/addon-storysource": "8.4.7",
|
"@storybook/addon-storysource": "8.5.0",
|
||||||
"@storybook/blocks": "8.4.7",
|
"@storybook/blocks": "8.5.0",
|
||||||
"@storybook/components": "8.4.7",
|
"@storybook/components": "8.5.0",
|
||||||
"@storybook/core-events": "8.4.7",
|
"@storybook/core-events": "8.5.0",
|
||||||
"@storybook/manager-api": "8.4.7",
|
"@storybook/manager-api": "8.5.0",
|
||||||
"@storybook/preview-api": "8.4.7",
|
"@storybook/preview-api": "8.5.0",
|
||||||
"@storybook/react": "8.4.7",
|
"@storybook/react": "8.5.0",
|
||||||
"@storybook/react-vite": "8.4.7",
|
"@storybook/react-vite": "8.5.0",
|
||||||
"@storybook/test": "8.4.7",
|
"@storybook/test": "8.5.0",
|
||||||
"@storybook/theming": "8.4.7",
|
"@storybook/theming": "8.5.0",
|
||||||
"@storybook/types": "8.4.7",
|
"@storybook/types": "8.5.0",
|
||||||
"@storybook/vue3": "8.4.7",
|
"@storybook/vue3": "8.5.0",
|
||||||
"@storybook/vue3-vite": "8.4.7",
|
"@storybook/vue3-vite": "8.5.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/escape-regexp": "0.0.3",
|
"@types/escape-regexp": "0.0.3",
|
||||||
"@types/estree": "1.0.6",
|
"@types/estree": "1.0.6",
|
||||||
"@types/matter-js": "0.19.8",
|
"@types/matter-js": "0.19.8",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.7",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/sanitize-html": "2.13.0",
|
"@types/sanitize-html": "2.13.0",
|
||||||
"@types/three": "0.171.0",
|
"@types/three": "0.172.0",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.5.13",
|
"@types/ws": "8.5.13",
|
||||||
|
@ -122,8 +122,8 @@
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-vue": "9.32.0",
|
"eslint-plugin-vue": "9.32.0",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.3",
|
||||||
"happy-dom": "16.0.1",
|
"happy-dom": "16.6.0",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"msw": "2.7.0",
|
"msw": "2.7.0",
|
||||||
|
@ -132,8 +132,8 @@
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.4.2",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"start-server-and-test": "2.0.9",
|
"start-server-and-test": "2.0.10",
|
||||||
"storybook": "8.4.7",
|
"storybook": "8.5.0",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "2.1.8",
|
"vitest": "2.1.8",
|
||||||
|
|
319
packages/frontend/src/components/MkDraftsDialog.vue
Normal file
319
packages/frontend/src/components/MkDraftsDialog.vue
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:height="500"
|
||||||
|
:width="800"
|
||||||
|
@click="done(true)"
|
||||||
|
@close="done(true)"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
{{ i18n.ts.drafts }}
|
||||||
|
</template>
|
||||||
|
<MkTab v-if="$i!.policies.canScheduleNote" v-model="tab" style="margin-bottom: var(--margin);">
|
||||||
|
<option value="unsent">{{ i18n.ts.unsent }}</option>
|
||||||
|
<option value="scheduled">{{ i18n.ts.scheduled }}</option>
|
||||||
|
</MkTab>
|
||||||
|
<div v-if="tab === 'unsent'" style="display: flex; flex-direction: column">
|
||||||
|
<div v-if="drafts.length === 0" class="empty">
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="draft in drafts" :key="draft.id" :class="[$style.draftItem, $style.draftItemHover]">
|
||||||
|
<div :class="$style.draftNote" @click="selectDraft(draft.id)">
|
||||||
|
<div :class="$style.draftNoteHeader">
|
||||||
|
<div :class="$style.draftNoteDestination">
|
||||||
|
<span v-if="draft.channel" style="opacity: 0.7; padding-right: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i> {{ draft.channel.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.renote">
|
||||||
|
<i class="ti ti-quote"></i> <MkAcct :user="draft.renote.user" /> <span>{{ draft.renote.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="draft.reply">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <MkAcct :user="draft.reply.user" /> <span>{{ draft.reply.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.draftNoteInfo">
|
||||||
|
<div style="display: flex; gap: 4px">
|
||||||
|
<div v-if="draft.scheduledAt" style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
|
<MkTime :time="draft.createdAt" colored />
|
||||||
|
</div>
|
||||||
|
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]">
|
||||||
|
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']">
|
||||||
|
<i class="ti ti-rocket-off"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.channel" :title="draft.channel.name">
|
||||||
|
<i class="ti ti-device-tv"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-if="!!draft.cw" :class="$style.draftNoteCw">
|
||||||
|
<Mfm :text="draft.cw" />
|
||||||
|
</p>
|
||||||
|
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-tooltip="i18n.ts.delete" :class="$style.button" class="_button" @click="removeDraft(draft.id)">
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MkPagination v-if="tab === 'scheduled'" ref="scheduledPaginationEl" :pagination="scheduledPagination">
|
||||||
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ items }">
|
||||||
|
<div v-for="draft in items.map(x => convertNoteDraftToNoteCompat(x))" :key="draft.id" :class="$style.draftItem">
|
||||||
|
<div :class="$style.draftNote">
|
||||||
|
<div :class="$style.draftNoteHeader">
|
||||||
|
<div :class="$style.draftNoteDestination">
|
||||||
|
<span v-if="draft.channel" style="opacity: 0.7; padding-right: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i> {{ draft.channel.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.renote">
|
||||||
|
<i class="ti ti-quote"></i> <MkAcct :user="draft.renote.user" /> <span>{{ draft.renote.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="draft.reply">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <MkAcct :user="draft.reply.user" /> <span>{{ draft.reply.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.draftNoteInfo">
|
||||||
|
<div style="display: flex; gap: 4px">
|
||||||
|
<div v-if="draft.scheduledAt" style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
|
<div v-else style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-exclamation-circle"/></span>
|
||||||
|
</div>
|
||||||
|
<MkTime :time="draft.createdAt" colored />
|
||||||
|
</div>
|
||||||
|
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]">
|
||||||
|
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']">
|
||||||
|
<i class="ti ti-rocket-off"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.channel" :title="draft.channel.name">
|
||||||
|
<i class="ti ti-device-tv"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-if="!!draft.cw" :class="$style.draftNoteCw">
|
||||||
|
<Mfm :text="draft.cw" />
|
||||||
|
</p>
|
||||||
|
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
||||||
|
<div v-if="draft.reason" style="opacity: 0.6; margin-top: 4px">
|
||||||
|
{{ i18n.ts.error }}: {{ draft.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-tooltip="i18n.ts.unschedule" :class="$style.button" class="_button" @click="unschedule(draft.id)">
|
||||||
|
<i class="ti ti-calendar-x"></i>
|
||||||
|
</button>
|
||||||
|
<button v-tooltip="i18n.ts.delete" :class="$style.button" class="_button" @click="cancelScheduled(draft.id)">
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted, ref, shallowRef } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkTab from '@/components/MkTab.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { canceled: true } | { canceled: false; selected: string | undefined }): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
const tab = ref('unsent');
|
||||||
|
|
||||||
|
const drafts = ref<(Misskey.entities.Note & { useCw: boolean, scheduledAt: string })[]>([]);
|
||||||
|
|
||||||
|
onMounted(loadDrafts);
|
||||||
|
onActivated(loadDrafts);
|
||||||
|
|
||||||
|
function convertNoteDraftToNoteCompat(draft: Misskey.entities.NoteDraft, key?: string) {
|
||||||
|
return {
|
||||||
|
...(draft.data as Misskey.entities.Note & { useCw: boolean }),
|
||||||
|
id: key ?? draft.id,
|
||||||
|
createdAt: draft.updatedAt,
|
||||||
|
scheduledAt: draft.scheduledAt,
|
||||||
|
reason: draft.reason,
|
||||||
|
channel: draft.channel as Misskey.entities.Channel,
|
||||||
|
renote: draft.renote as Misskey.entities.Note,
|
||||||
|
reply: draft.reply as Misskey.entities.Note,
|
||||||
|
user: $i as Misskey.entities.User,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDrafts() {
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
drafts.value = Object.keys(stored).map((key) => convertNoteDraftToNoteCompat(stored[key], key));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDraft(draft: string) {
|
||||||
|
done(false, draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDraft(draft: string) {
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
|
delete stored[draft];
|
||||||
|
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
||||||
|
|
||||||
|
loadDrafts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unschedule(draft: string) {
|
||||||
|
const item = scheduledPaginationEl.value!.items.find(x => x.id === draft);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
let key = item.channel ? `channel:${item.channel.id}` : '';
|
||||||
|
if (item.renote) {
|
||||||
|
key += `renote:${item.renote.id}`;
|
||||||
|
} else if (item.reply) {
|
||||||
|
key += `reply:${item.reply.id}`;
|
||||||
|
} else {
|
||||||
|
key += `note:${item.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
|
stored[key] = item as unknown as Misskey.entities.NoteDraft;
|
||||||
|
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
||||||
|
|
||||||
|
cancelScheduled(item.id);
|
||||||
|
loadDrafts();
|
||||||
|
tab.value = 'unsent';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelScheduled(draft: string) {
|
||||||
|
os.apiWithDialog('notes/scheduled/cancel', {
|
||||||
|
draftId: draft,
|
||||||
|
}).then(() => {
|
||||||
|
scheduledPaginationEl.value?.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(canceled: boolean, selected?: string): void {
|
||||||
|
emit('done', { canceled, selected } as
|
||||||
|
| { canceled: true }
|
||||||
|
| { canceled: false; selected: string | undefined });
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledPaginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
const scheduledPagination = {
|
||||||
|
endpoint: 'notes/scheduled/list' as const,
|
||||||
|
offsetMode: true,
|
||||||
|
limit: 10,
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.draftItem {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0 8px 0;
|
||||||
|
border-bottom: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftItemHover {
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNote {
|
||||||
|
flex: 1;
|
||||||
|
width: calc(100% - 16px - 48px - 4px);
|
||||||
|
margin: 0 8px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteDestination {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteCw {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteText {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 48px;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--buttonBg);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
|
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted, defineAsyncComponent } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
@ -87,11 +87,11 @@ import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
import MkAudioVisualizer from '@/components/MkAudioVisualizer.vue';
|
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
|
|
||||||
|
const MkAudioVisualizer = defineAsyncComponent(() => import('@/components/MkAudioVisualizer.vue'));
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
audio: Misskey.entities.DriveFile;
|
audio: Misskey.entities.DriveFile;
|
||||||
user?: Misskey.entities.UserLite;
|
user?: Misskey.entities.UserLite;
|
||||||
|
|
|
@ -569,13 +569,8 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
content-visibility: auto;
|
||||||
// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
|
contain-intrinsic-size: auto none auto 128px;
|
||||||
// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
|
|
||||||
// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
|
|
||||||
// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
|
|
||||||
//content-visibility: auto;
|
|
||||||
//contain-intrinsic-size: 0 128px;
|
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -25,6 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_quote]: notification.type === 'quote',
|
[$style.t_quote]: notification.type === 'quote',
|
||||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
[$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,
|
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
|
@ -37,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
|
<i v-else-if="notification.type === 'noteScheduled'" class="ti ti-calendar-clock"></i>
|
||||||
|
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-calendar-check"></i>
|
||||||
|
<i v-else-if="notification.type === 'scheduledNoteError'" class="ti ti-calendar-exclamation"></i>
|
||||||
<template v-else-if="notification.type === 'roleAssigned'">
|
<template v-else-if="notification.type === 'roleAssigned'">
|
||||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||||
<i v-else class="ti ti-badges"></i>
|
<i v-else class="ti ti-badges"></i>
|
||||||
|
@ -52,16 +58,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.tail">
|
<div :class="$style.tail">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'" :class="$style.headerName">{{ i18n.ts._notification.pollEnded }}</span>
|
||||||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
<span v-else-if="notification.type === 'note'" :class="$style.headerName">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
<span v-else-if="notification.type === 'roleAssigned'" :class="$style.headerName">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'" :class="$style.headerName">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'noteScheduled'" :class="$style.headerName">{{ i18n.ts._notification.noteScheduled }}</span>
|
||||||
|
<span v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.headerName">{{ i18n.ts._notification.scheduledNotePosted }}</span>
|
||||||
|
<span v-else-if="notification.type === 'scheduledNoteError'" :class="$style.headerName">{{ i18n.ts._notification.scheduledNoteError }}</span>
|
||||||
|
<span v-else-if="notification.type === 'test'" :class="$style.headerName">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="$style.headerName">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
<span v-else-if="notification.type === 'renote:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
<span v-else-if="notification.type === 'app'" :class="$style.headerName">{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
|
@ -98,6 +107,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<div v-else-if="notification.type === 'noteScheduled'">
|
||||||
|
<Mfm :class="$style.text" :text="getNoteSummary(notification.draft.data as unknown as Misskey.entities.Note)" :plain="true" :nowrap="true"/>
|
||||||
|
<div v-if="notification.draft.scheduledAt" :class="$style.text" style="opacity: 0.6;">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="notification.draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
|
</MkA>
|
||||||
|
<div v-else-if="notification.type === 'scheduledNoteError'">
|
||||||
|
<Mfm :class="$style.text" :text="getNoteSummary(notification.draft.data as unknown as Misskey.entities.Note)" :plain="true" :nowrap="true"/>
|
||||||
|
<div v-if="notification.draft.reason" :class="$style.text" style="opacity: 0.6;">
|
||||||
|
<span><i class="ti ti-exclamation-circle" style="margin-right: 4px;"/></span>
|
||||||
|
{{ notification.draft.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
||||||
|
@ -192,6 +218,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto none auto 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
|
@ -300,6 +329,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_noteScheduled, .t_scheduledNotePosted, .t_scheduledNoteError {
|
||||||
|
padding: 3px;
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.t_roleAssigned {
|
.t_roleAssigned {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: var(--eventOther);
|
background: var(--eventOther);
|
||||||
|
|
|
@ -41,12 +41,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
|
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
|
||||||
<span v-else><i class="ti ti-icons"></i></span>
|
<span v-else><i class="ti ti-icons"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="!props.instant" v-click-anime v-tooltip="i18n.ts.drafts" class="_button" :class="$style.headerRightItem" @click="openDrafts">
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</button>
|
||||||
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||||
<div :class="$style.submitInner">
|
<div :class="$style.submitInner">
|
||||||
<template v-if="posted"></template>
|
<template v-if="posted"></template>
|
||||||
<template v-else-if="posting"><MkEllipsis/></template>
|
<template v-else-if="posting"><MkEllipsis/></template>
|
||||||
<template v-else>{{ submitText }}</template>
|
<template v-else>
|
||||||
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
|
<span :class="$style.submitButtonText">
|
||||||
|
{{ submitText }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
<i :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +81,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="mk-input-text" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="mk-input-text" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<MkInfo v-if="files.length > 0" warn :class="$style.guidelineInfo" :rounded="false"><Mfm :text="i18n.tsx._postForm.guidelineInfo({ tosUrl: instance.tosUrl, nsfwGuideUrl })"/></MkInfo>
|
<div v-if="scheduledTime" :class="$style.scheduledTime">
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; gap: 4px" :style="scheduledTimeExceededPolicy ? 'color: var(--error)' : undefined">
|
||||||
|
<span style="margin-right: 4px"><i class="ti ti-calendar-clock"></i></span>
|
||||||
|
<component :is="scheduledTimeExceededPolicy ? 'del' : 'span'" :style="scheduledTimeExceededPolicy ? 'opacity: 0.6' : undefined">
|
||||||
|
{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
<div v-if="scheduledTimeExceededPolicy" style="display: flex; gap: 4px; margin-top: 4px; color: var(--infoWarnFg)">
|
||||||
|
<span style="margin-right: 4px"><i class="ti ti-exclamation-circle"></i></span>
|
||||||
|
<Mfm :text="i18n.tsx._postForm.policyScheduleNoteMaxDaysExceeded({ max: $i.policies.scheduleNoteMaxDays })"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="_button" style="margin-left: auto" @click="scheduledTime = null"><i class="ti ti-x"></i></button>
|
||||||
|
</div>
|
||||||
|
<MkInfo v-if="files.length > 0 && instance.tosUrl" warn style="margin-top: 8px;" :rounded="false">
|
||||||
|
<Mfm :text="i18n.tsx._postForm.tosAndGuidelinesInfo({ tosUrl: instance.tosUrl })"/>
|
||||||
|
</MkInfo>
|
||||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||||
|
@ -85,6 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
|
<button v-if="$i.policies.canScheduleNote" v-tooltip="i18n.ts.setScheduledTime" class="_button" :class="$style.footerButton" @click="setScheduledTime"><i class="ti ti-calendar-clock"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||||
|
@ -110,6 +137,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
|
import MkDraftsDialog from '@/components/MkDraftsDialog.vue';
|
||||||
import { host, url } from '@/config.js';
|
import { host, url } from '@/config.js';
|
||||||
import { erase, unique } from '@/scripts/array.js';
|
import { erase, unique } from '@/scripts/array.js';
|
||||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||||
|
@ -127,8 +155,8 @@ import { uploadFile } from '@/scripts/upload.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
|
||||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
@ -180,6 +208,10 @@ const visibilityButton = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const posting = ref(false);
|
const posting = ref(false);
|
||||||
const posted = ref(false);
|
const posted = ref(false);
|
||||||
|
const draftId = ref<string>(Date.now().toString());
|
||||||
|
const reply = ref(props.reply ?? null);
|
||||||
|
const renote = ref(props.renote ?? null);
|
||||||
|
const channel = ref(props.channel ?? null);
|
||||||
const text = ref(props.initialText ?? '');
|
const text = ref(props.initialText ?? '');
|
||||||
const files = ref(props.initialFiles ?? []);
|
const files = ref(props.initialFiles ?? []);
|
||||||
const poll = ref<PollEditorModelValue | null>(null);
|
const poll = ref<PollEditorModelValue | null>(null);
|
||||||
|
@ -196,6 +228,10 @@ if (props.initialVisibleUsers) {
|
||||||
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
||||||
}
|
}
|
||||||
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
||||||
|
const scheduledTime = ref<Date | null>(null);
|
||||||
|
const scheduledTimeExceededPolicy = computed(() =>
|
||||||
|
scheduledTime.value ? (scheduledTime.value.getTime() - Date.now()) / 86_400_000 > $i!.policies.scheduleNoteMaxDays : false
|
||||||
|
);
|
||||||
const autocompleteTextareaInput = ref<Autocomplete | null>(null);
|
const autocompleteTextareaInput = ref<Autocomplete | null>(null);
|
||||||
const autocompleteCwInput = ref<Autocomplete | null>(null);
|
const autocompleteCwInput = ref<Autocomplete | null>(null);
|
||||||
const autocompleteHashtagsInput = ref<Autocomplete | null>(null);
|
const autocompleteHashtagsInput = ref<Autocomplete | null>(null);
|
||||||
|
@ -207,28 +243,26 @@ const imeText = ref('');
|
||||||
const showingOptions = ref(false);
|
const showingOptions = ref(false);
|
||||||
const textAreaReadOnly = ref(false);
|
const textAreaReadOnly = ref(false);
|
||||||
|
|
||||||
const nsfwGuideUrl = 'https://go.misskey.io/media-guideline';
|
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = channel.value ? `channel:${channel.value.id}` : '';
|
||||||
|
|
||||||
if (props.renote) {
|
if (renote.value) {
|
||||||
key += `renote:${props.renote.id}`;
|
key += `renote:${renote.value.id}`;
|
||||||
} else if (props.reply) {
|
} else if (reply.value) {
|
||||||
key += `reply:${props.reply.id}`;
|
key += `reply:${reply.value.id}`;
|
||||||
} else {
|
} else {
|
||||||
key += `note:${$i.id}`;
|
key += `note:${draftId.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
const placeholder = computed((): string => {
|
const placeholder = computed((): string => {
|
||||||
if (props.renote) {
|
if (renote.value) {
|
||||||
return i18n.ts._postForm.quotePlaceholder;
|
return i18n.ts._postForm.quotePlaceholder;
|
||||||
} else if (props.reply) {
|
} else if (reply.value) {
|
||||||
return i18n.ts._postForm.replyPlaceholder;
|
return i18n.ts._postForm.replyPlaceholder;
|
||||||
} else if (props.channel) {
|
} else if (channel.value) {
|
||||||
return i18n.ts._postForm.channelPlaceholder;
|
return i18n.ts._postForm.channelPlaceholder;
|
||||||
} else {
|
} else {
|
||||||
const xs = [
|
const xs = [
|
||||||
|
@ -244,11 +278,15 @@ const placeholder = computed((): string => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitText = computed((): string => {
|
const submitText = computed((): string => {
|
||||||
return props.renote
|
if (scheduledTime.value) {
|
||||||
? i18n.ts.quote
|
return i18n.ts.schedule;
|
||||||
: props.reply
|
} else if (renote.value) {
|
||||||
? i18n.ts.reply
|
return i18n.ts.quote;
|
||||||
: i18n.ts.note;
|
} else if (reply.value) {
|
||||||
|
return i18n.ts.reply;
|
||||||
|
} else {
|
||||||
|
return i18n.ts.note;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const textLength = computed((): number => {
|
const textLength = computed((): number => {
|
||||||
|
@ -260,16 +298,20 @@ const maxTextLength = computed((): number => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const canPost = computed((): boolean => {
|
const canPost = computed((): boolean => {
|
||||||
return !props.mock && !posting.value && !posted.value &&
|
return !props.mock
|
||||||
(
|
&& !posting.value
|
||||||
|
&& !posted.value
|
||||||
|
&& (
|
||||||
1 <= textLength.value ||
|
1 <= textLength.value ||
|
||||||
1 <= files.value.length ||
|
1 <= files.value.length ||
|
||||||
poll.value != null ||
|
poll.value != null ||
|
||||||
props.renote != null ||
|
renote.value != null ||
|
||||||
(props.reply != null && quoteId.value != null)
|
(reply.value != null && quoteId.value != null)
|
||||||
) &&
|
)
|
||||||
(textLength.value <= maxTextLength.value) &&
|
&& (textLength.value <= maxTextLength.value)
|
||||||
(!poll.value || poll.value.choices.length >= 2);
|
&& (!poll.value || poll.value.choices.length >= 2)
|
||||||
|
&& !scheduledTimeExceededPolicy.value
|
||||||
|
;
|
||||||
});
|
});
|
||||||
|
|
||||||
const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
|
const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
|
||||||
|
@ -294,13 +336,13 @@ if (props.mention) {
|
||||||
text.value += ' ';
|
text.value += ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
|
if (reply.value && (reply.value.user.username !== $i.username || (reply.value.user.host != null && reply.value.user.host !== host))) {
|
||||||
text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
|
text.value = `@${reply.value.user.username}${reply.value.user.host != null ? '@' + toASCII(reply.value.user.host) : ''} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply && props.reply.text != null) {
|
if (reply.value && reply.value.text != null) {
|
||||||
const ast = mfm.parse(props.reply.text);
|
const ast = mfm.parse(reply.value.text);
|
||||||
const otherHost = props.reply.user.host;
|
const otherHost = reply.value.user.host;
|
||||||
|
|
||||||
for (const x of extractMentions(ast)) {
|
for (const x of extractMentions(ast)) {
|
||||||
const mention = x.host ?
|
const mention = x.host ?
|
||||||
|
@ -323,32 +365,32 @@ if ($i.isSilenced && visibility.value === 'public') {
|
||||||
visibility.value = 'home';
|
visibility.value = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||||
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
|
if (reply.value && ['home', 'followers', 'specified'].includes(reply.value.visibility)) {
|
||||||
if (props.reply.visibility === 'home' && visibility.value === 'followers') {
|
if (reply.value.visibility === 'home' && visibility.value === 'followers') {
|
||||||
visibility.value = 'followers';
|
visibility.value = 'followers';
|
||||||
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') {
|
} else if (['home', 'followers'].includes(reply.value.visibility) && visibility.value === 'specified') {
|
||||||
visibility.value = 'specified';
|
visibility.value = 'specified';
|
||||||
} else {
|
} else {
|
||||||
visibility.value = props.reply.visibility;
|
visibility.value = reply.value.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibility.value === 'specified') {
|
if (visibility.value === 'specified') {
|
||||||
if (props.reply.visibleUserIds) {
|
if (reply.value.visibleUserIds) {
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
|
userIds: reply.value.visibleUserIds.filter(uid => uid !== $i.id && uid !== reply.value?.userId),
|
||||||
}).then(users => {
|
}).then(users => {
|
||||||
users.forEach(u => pushVisibleUser(u));
|
users.forEach(u => pushVisibleUser(u));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply.userId !== $i.id) {
|
if (reply.value.userId !== $i.id) {
|
||||||
misskeyApi('users/show', { userId: props.reply.userId }).then(user => {
|
misskeyApi('users/show', { userId: reply.value.userId }).then(user => {
|
||||||
pushVisibleUser(user);
|
pushVisibleUser(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -361,9 +403,9 @@ if (props.specified) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep cw when reply
|
// keep cw when reply
|
||||||
if (defaultStore.state.keepCw && props.reply?.cw) {
|
if (defaultStore.state.keepCw && reply.value?.cw) {
|
||||||
useCw.value = true;
|
useCw.value = true;
|
||||||
cw.value = props.reply.cw;
|
cw.value = reply.value.cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchForDraft() {
|
function watchForDraft() {
|
||||||
|
@ -374,6 +416,7 @@ function watchForDraft() {
|
||||||
watch(files, () => saveDraft(), { deep: true });
|
watch(files, () => saveDraft(), { deep: true });
|
||||||
watch(visibility, () => saveDraft());
|
watch(visibility, () => saveDraft());
|
||||||
watch(localOnly, () => saveDraft());
|
watch(localOnly, () => saveDraft());
|
||||||
|
watch(scheduledTime, () => saveDraft());
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMissingMention() {
|
function checkMissingMention() {
|
||||||
|
@ -465,7 +508,7 @@ function upload(file: File, name?: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
return;
|
return;
|
||||||
|
@ -476,7 +519,7 @@ function setVisibility() {
|
||||||
isSilenced: $i.isSilenced,
|
isSilenced: $i.isSilenced,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
src: visibilityButton.value,
|
src: visibilityButton.value,
|
||||||
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
...(reply.value ? { isReplyVisibilitySpecified: reply.value.visibility === 'specified' } : {}),
|
||||||
}, {
|
}, {
|
||||||
changeVisibility: v => {
|
changeVisibility: v => {
|
||||||
visibility.value = v;
|
visibility.value = v;
|
||||||
|
@ -488,7 +531,7 @@ function setVisibility() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleLocalOnly() {
|
async function toggleLocalOnly() {
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
return;
|
return;
|
||||||
|
@ -568,10 +611,26 @@ function removeVisibleUser(user) {
|
||||||
visibleUsers.value = erase(user, visibleUsers.value);
|
visibleUsers.value = erase(user, visibleUsers.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setScheduledTime() {
|
||||||
|
const { canceled, result: date } = await os.inputDateTime({
|
||||||
|
title: i18n.ts.setScheduledTime,
|
||||||
|
default: scheduledTime.value ?? undefined,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
scheduledTime.value = date;
|
||||||
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
text.value = '';
|
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 = [];
|
files.value = [];
|
||||||
poll.value = null;
|
poll.value = null;
|
||||||
|
visibleUsers.value = [];
|
||||||
|
scheduledTime.value = null;
|
||||||
quoteId.value = null;
|
quoteId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +664,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
|
|
||||||
const paste = ev.clipboardData.getData('text');
|
const paste = ev.clipboardData.getData('text');
|
||||||
|
|
||||||
if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
|
if (!renote.value && !quoteId.value && paste.startsWith(url + '/notes/')) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
os.confirm({
|
os.confirm({
|
||||||
|
@ -679,10 +738,38 @@ function onDrop(ev: DragEvent): void {
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
if (props.instant || props.mock) return;
|
if (props.instant || props.mock) return;
|
||||||
|
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
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<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
draftData[draftKey.value] = {
|
draftData[draftKey.value] = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
scheduledAt: scheduledAt?.toISOString() ?? null,
|
||||||
|
channel: channel.value ? {
|
||||||
|
id: channel.value.id,
|
||||||
|
name: channel.value.name,
|
||||||
|
} : undefined,
|
||||||
|
renote: renote.value ? {
|
||||||
|
id: renote.value.id,
|
||||||
|
text: (renote.value.cw ?? renote.value.text)?.substring(0, 100),
|
||||||
|
user: {
|
||||||
|
id: renote.value.userId,
|
||||||
|
username: renote.value.user.username,
|
||||||
|
host: renote.value.user.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
reply: reply.value ? {
|
||||||
|
id: reply.value.id,
|
||||||
|
text: (reply.value.cw ?? reply.value.text)?.substring(0, 100),
|
||||||
|
user: {
|
||||||
|
id: reply.value.userId,
|
||||||
|
username: reply.value.user.username,
|
||||||
|
host: reply.value.user.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
data: {
|
data: {
|
||||||
text: text.value,
|
text: text.value,
|
||||||
useCw: useCw.value,
|
useCw: useCw.value,
|
||||||
|
@ -700,13 +787,79 @@ function saveDraft() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDraft() {
|
function deleteDraft() {
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
delete draftData[draftKey.value];
|
delete draftData[draftKey.value];
|
||||||
|
|
||||||
|
draftId.value = Date.now().toString();
|
||||||
|
|
||||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openDrafts() {
|
||||||
|
const { canceled, selected } = await new Promise<{canceled: boolean, selected: string | undefined}>(resolve => {
|
||||||
|
os.popup(MkDraftsDialog, {}, {
|
||||||
|
done: result => {
|
||||||
|
resolve(typeof result.selected === 'string' ? result : { canceled: true, selected: undefined });
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
const channelId = selected.startsWith('channel:') ? selected.match(/channel:(.+?)(renote|reply|note):/)?.[1] : undefined;
|
||||||
|
const renoteId = selected.includes('renote:') ? selected.match(/renote:(.+)/)?.[1] : undefined;
|
||||||
|
const replyId = selected.includes('reply:') ? selected.match(/reply:(.+)/)?.[1] : undefined;
|
||||||
|
|
||||||
|
channel.value = channelId ? await misskeyApi('channels/show', { channelId }) : null;
|
||||||
|
renote.value = renoteId ? await misskeyApi('notes/show', { noteId: renoteId }) : null;
|
||||||
|
reply.value = replyId ? await misskeyApi('notes/show', { noteId: replyId }) : null;
|
||||||
|
|
||||||
|
if (!renote.value && !reply.value) {
|
||||||
|
draftId.value = selected.match(/note:(.+)/)?.[1] ?? Date.now().toString();
|
||||||
|
} else {
|
||||||
|
draftId.value = Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDraft(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDraft(exactMatch = false) {
|
||||||
|
const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
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() } }))
|
||||||
|
.sort((a, b) => b.value.updatedAt - a.value.updatedAt).at(0);
|
||||||
|
|
||||||
|
if (draft) {
|
||||||
|
if (scope !== draft.key) {
|
||||||
|
draftId.value = draft.key.replace(scope, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
localOnly.value = draft.value.data.localOnly;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
files.value = draft.value.data.files?.filter(f => f?.id && f.type && f.name) || [];
|
||||||
|
if (draft.value.data.poll) {
|
||||||
|
poll.value = draft.value.data.poll;
|
||||||
|
}
|
||||||
|
if (draft.value.data.visibleUserIds) {
|
||||||
|
misskeyApi('users/show', { userIds: draft.value.data.visibleUserIds }).then(users => {
|
||||||
|
users.forEach(u => pushVisibleUser(u));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function post(ev?: MouseEvent) {
|
async function post(ev?: MouseEvent) {
|
||||||
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
|
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
|
||||||
os.alert({
|
os.alert({
|
||||||
|
@ -789,15 +942,16 @@ async function post(ev?: MouseEvent) {
|
||||||
text: text.value === '' ? null : text.value,
|
text: text.value === '' ? null : text.value,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined,
|
||||||
replyId: props.reply ? props.reply.id : undefined,
|
replyId: reply.value ? reply.value.id : undefined,
|
||||||
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
renoteId: renote.value ? renote.value.id : quoteId.value ? quoteId.value : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: channel.value ? channel.value.id : undefined,
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
cw: useCw.value ? cw.value ?? '' : null,
|
cw: useCw.value ? cw.value ?? '' : null,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
scheduledAt: scheduledTime.value?.getTime() ?? undefined,
|
||||||
noCreatedNote: true,
|
noCreatedNote: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -879,7 +1033,7 @@ async function post(ev?: MouseEvent) {
|
||||||
claimAchievement('brainDiver');
|
claimAchievement('brainDiver');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.renote && (props.renote.userId === $i.id) && text.length > 0) {
|
if (renote.value && (renote.value.userId === $i.id) && text.length > 0) {
|
||||||
claimAchievement('selfQuote');
|
claimAchievement('selfQuote');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -982,25 +1136,8 @@ onMounted(() => {
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 書きかけの投稿を復元
|
// 書きかけの投稿を復元
|
||||||
if (!props.instant && !props.mention && !props.specified && !props.mock) {
|
if (!props.instant && !props.mention && !props.specified && !props.mock && defaultStore.state.autoloadDrafts) {
|
||||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value];
|
loadDraft();
|
||||||
if (draft) {
|
|
||||||
text.value = draft.data.text;
|
|
||||||
useCw.value = draft.data.useCw;
|
|
||||||
cw.value = draft.data.cw;
|
|
||||||
visibility.value = draft.data.visibility;
|
|
||||||
localOnly.value = draft.data.localOnly;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
files.value = draft.data.files?.filter(f => f?.id && f.type && f.name) || [];
|
|
||||||
if (draft.data.poll) {
|
|
||||||
poll.value = draft.data.poll;
|
|
||||||
}
|
|
||||||
if (draft.data.visibleUserIds) {
|
|
||||||
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
|
|
||||||
users.forEach(u => pushVisibleUser(u));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 削除して編集
|
// 削除して編集
|
||||||
|
@ -1022,6 +1159,7 @@ onMounted(() => {
|
||||||
visibility.value = init.visibility;
|
visibility.value = init.visibility;
|
||||||
localOnly.value = init.localOnly ?? false;
|
localOnly.value = init.localOnly ?? false;
|
||||||
quoteId.value = init.renote ? init.renote.id : null;
|
quoteId.value = init.renote ? init.renote.id : null;
|
||||||
|
scheduledTime.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => watchForDraft());
|
nextTick(() => watchForDraft());
|
||||||
|
@ -1143,6 +1281,10 @@ defineExpose({
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--fgOnAccent);
|
color: var(--fgOnAccent);
|
||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerRightItem {
|
.headerRightItem {
|
||||||
|
@ -1291,6 +1433,15 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scheduledTime {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 90%;
|
||||||
|
background: var(--infoBg);
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
|
@ -1337,10 +1488,6 @@ defineExpose({
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.guidelineInfo {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
@container (max-width: 500px) {
|
||||||
.headerRight {
|
.headerRight {
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
|
@ -1380,6 +1527,16 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 375px) {
|
||||||
|
.submitInner {
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButtonText {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@container (max-width: 350px) {
|
@container (max-width: 350px) {
|
||||||
.footer {
|
.footer {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
@ -1396,6 +1553,5 @@ defineExpose({
|
||||||
.headerRight {
|
.headerRight {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue