1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-12-18 00:29:04 +09:00

Merge remote-tracking branch 'origin/develop'

This commit is contained in:
NoriDev 2023-09-29 22:49:47 +09:00
commit f49eb46f48
155 changed files with 2718 additions and 2380 deletions

View File

@ -60,17 +60,17 @@ dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host: localhost
# port: 5433
# db: cherrypick
# user: example-cherrypick-user
# pass: example-cherrypick-pass
# host:
# port:
# db:
# user:
# pass:
# -
# host: localhost
# port: 5434
# db: cherrypick
# user: example-cherrypick-user
# pass: example-cherrypick-pass
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@ -215,3 +215,6 @@ signToActivityPubGet: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
# PID File of master process
#pidFile: /tmp/misskey.pid

View File

@ -12,6 +12,28 @@
-->
## 2023.9.2
### General
- Feat: ノートの編集をできるように
- ロールで編集可否を設定可能
- Feat: 通知を種類ごとに 全員から受け取る/フォロー中のユーザーのみ受け取る/フォロワーのみ受け取る/相互のみ受け取る/指定したリストのメンバーのみ受け取る/受け取らない から選べるように
- Enhance: タイムラインからRenoteを除外するオプションを追加
- Enhance: ユーザーページのート一覧でRenoteを除外できるように
- Enhance: タイムラインでファイルが添付されたノートのみ表示するオプションを追加
- Enhance: モデレーションログ機能の強化
- Enhance: 依存関係の更新
- Enhance: ローカリゼーションの更新
### Client
- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように
- Enhance: admin/ad/listにて掲載中の広告が絞り込めるように
- Enhance: AiScriptにリモートサーバーのAPIを叩く用の関数を追加`Mk:apiExternal`
### Server
- Enhance: MasterプロセスのPIDを書き出せるように
- Enhance: admin/ad/createにてレスポンス200、設定した広告情報を返すように
## 2023.9.1
### General

View File

@ -22,6 +22,17 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
# 릴리즈 노트
이 문서는 CherryPick의 변경 사항만 포함합니다.
## 4.3.1
출시일: 2023/09/29<br>
기반 Misskey 버전: 2023.9.2<br>
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202392](CHANGELOG.md#202392) 문서를 참고하십시오.
### General
- 미디어, 고양이 타임라인 개선
- [misskey-dev/misskey@eb740e2](https://github.com/misskey-dev/misskey/commit/eb740e2c72ae6854b244ad099c927c069008720e) 이 추가됨에 따라, 해당 기능에 병합하고 기존 미디어 및 고양이 타임라인을 제거함
---
## 4.3.0
출시일: 2023/09/29<br>
기반 Misskey 버전: 2023.9.1<br>

View File

@ -2039,3 +2039,4 @@ _webhookSettings:
_moderationLogTypes:
suspend: "Zmrazit"
resetPassword: "Resetovat heslo"
createInvitation: "Vygenerovat pozvánku"

View File

@ -1134,6 +1134,9 @@ notifyNotes: "Über neue Notizen benachrichtigen"
unnotifyNotes: "Nicht über neue Notizen benachrichtigen"
authentication: "Authentifikation"
authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren"
dateAndTime: "Zeit"
showRenotes: "Renotes anzeigen"
edited: "Bearbeitet"
_announcement:
forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1464,6 +1467,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
canEditNote: "Notizbearbeitung"
canInvite: "Erstellung von Einladungscodes für diese Instanz"
inviteLimit: "Maximalanzahl an Einladungen"
inviteLimitCycle: "Zyklus des Einladungslimits"
@ -2187,6 +2191,8 @@ _webhookSettings:
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"
_moderationLogTypes:
createRole: "Rolle erstellt"
deleteRole: "Rolle gelöscht"
updateRole: "Rolle aktualisiert"
assignRole: "Zu Rolle zugewiesen"
unassignRole: "Aus Rolle entfernt"
@ -2210,3 +2216,5 @@ _moderationLogTypes:
unsuspendRemoteInstance: "Fremde Instanz entsperrt"
markSensitiveDriveFile: "Datei als sensitiv markiert"
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"
resolveAbuseReport: "Meldung bearbeitet"
createInvitation: "Einladung erstellt"

View File

@ -1197,6 +1197,10 @@ notifyNotes: "Notify about new notes"
unnotifyNotes: "Stop notifying about new notes"
authentication: "Authentication"
authenticationRequiredToContinue: "Please authenticate to continue"
dateAndTime: "Timestamp"
showRenotes: "Show renotes"
edited: "Edited"
showCatOnly: "Show only cats"
additionalPermissionsForFlash: "Allow to add permission to Play"
thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions"
doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?"
@ -1206,10 +1210,8 @@ _messaging:
_tlTutorial:
step1_1: 'The {icon} Home timeline is where you can see posts from the accounts you follow.'
step1_2: 'The {icon} Local timeline is where you can see posts from everyone else on this server.'
step1_3: 'The {icon} Media timeline is where you can see posts from everyone else on this server, but only those that contain media(photos, videos).'
step1_4: 'The {icon} Social timeline is a combination of the Home and Local timelines.'
step1_5: 'The {icon} Cat timeline shows posts from users who have enabled cat on their social timeline.'
step1_6: 'The {icon} Global timeline is where you can see posts from every other connected server.'
step1_3: 'The {icon} Social timeline is a combination of the Home and Local timelines.'
step1_4: 'The {icon} Global timeline is where you can see posts from every other connected server.'
_announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1610,9 +1612,8 @@ _role:
_options:
gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline"
mtlAvailable: "Can view the media timeline"
ctlAvailable: "Can view the cat timeline"
canPublicNote: "Can send public notes"
canEditNote: "Note editing"
canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"
@ -2196,9 +2197,7 @@ _instanceCharts:
_timelines:
home: "Home"
local: "Local"
media: "Media"
social: "Social"
cat: "Cat"
global: "Global"
_play:
new: "Create Play"
@ -2363,6 +2362,8 @@ _webhookSettings:
reaction: "When receiving a reaction"
mention: "When being mentioned"
_moderationLogTypes:
createRole: "Role created"
deleteRole: "Role deleted"
updateRole: "Role updated"
assignRole: "Assigned to role"
unassignRole: "Removed from role"
@ -2386,6 +2387,8 @@ _moderationLogTypes:
unsuspendRemoteInstance: "Remote instance unsuspended"
markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive"
resolveAbuseReport: "Report resolved"
createInvitation: "Invite generated"
_abuse:
_resolver:
1hour: "one hour"

View File

@ -418,6 +418,7 @@ moderator: "Moderador"
moderation: "Moderación"
moderationNote: "Nota de moderación"
addModerationNote: "Añadir nota de moderación"
moderationLogs: "Log de moderación"
nUsersMentioned: "{n} usuarios mencionados"
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
securityKey: "Clave de seguridad"
@ -722,6 +723,7 @@ lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"S
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
disableShowingAnimatedImages: "No reproducir imágenes animadas"
highlightSensitiveMedia: "Resaltar medios marcados como sensibles"
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
notSet: "Sin especificar"
emailVerified: "Su dirección de correo electrónico ha sido verificada."
@ -1123,6 +1125,16 @@ youHaveUnreadAnnouncements: "Hay anuncios sin leer"
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
replies: "Responder"
renotes: "Renotar"
loadReplies: "Ver respuestas"
loadConversation: "Ver conversación"
pinnedList: "Lista fijada"
keepScreenOn: "Mantener pantalla encendida"
verifiedLink: "Propiedad del enlace verificada"
notifyNotes: "Notificar nuevas notas"
unnotifyNotes: "Dejar de notificar nuevas notas"
authentication: "Autenticación"
authenticationRequiredToContinue: "Por favor, autentifícate para continuar"
dateAndTime: "Fecha y hora"
_announcement:
forExistingUsers: "Solo para usuarios registrados"
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
@ -1151,7 +1163,13 @@ _serverRules:
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
_serverSettings:
iconUrl: "URL del ícono"
appIconDescription: "Indica el icono que se va a usar cuando {host} se muestre como una app."
appIconUsageExample: "Por ejemplo, como PWA o cuando se muestre como un marcador en la pantalla inicial del dispositivo"
appIconStyleRecommendation: "Como el icono puede ser recortado como un cuadrado o un círculo, se recomienda un icono con un margen coloreado alrededor del contenido."
appIconResolutionMustBe: "La resolución mínima es {resolution}."
manifestJsonOverride: "Sobreescribir manifest.json"
shortName: "Nombre corto"
shortNameDescription: "Forma corta del nombre de la instancia que puede mostrarse si el nombre completo es demasiado largo."
_accountMigration:
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromSub: "Crear un alias para otra cuenta."
@ -1865,6 +1883,7 @@ _antennaSources:
users: "Notas de un usuario o varios"
userList: "Notas de los usuarios de una lista"
userGroup: "Notas de los usuarios de una grupo"
userBlacklist: "Todas las notas excepto aquellas de uno o más usuarios especificados"
_weekday:
sunday: "Domingo"
monday: "Lunes"
@ -1964,6 +1983,7 @@ _profile:
metadataContent: "Contenido"
changeAvatar: "Cambiar avatar"
changeBanner: "Cambiar banner"
verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo."
_exportOrImport:
allNotes: "Todas las notas"
favoritedNotes: "Notas favoritas"
@ -2086,6 +2106,7 @@ _notification:
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
youWereInvitedToGroup: "Invitado al grupo"
pollEnded: "Estan disponibles los resultados de la encuesta"
newNote: "Nueva nota"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
@ -2095,6 +2116,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
_types:
all: "Todo"
note: "Nuevas notas"
follow: "Siguiendo"
mention: "Menciones"
reply: "Respuestas"
@ -2166,5 +2188,30 @@ _webhookSettings:
reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención"
_moderationLogTypes:
createRole: "Rol creado"
deleteRole: "Rol eliminado"
updateRole: "Rol actualizado"
assignRole: "Rol asignado"
unassignRole: "Rol retirado"
suspend: "Suspender"
unsuspend: "Suspensión retirada"
addCustomEmoji: "Añadido emoji personalizado"
updateCustomEmoji: "Emoji personalizado actualizado"
deleteCustomEmoji: "Emoji personalizado eliminado"
updateServerSettings: "Ajustes de servidor actualizados"
updateUserNote: "Nota de moderación actualizada"
deleteDriveFile: "Archivo eliminado"
deleteNote: "Nota eliminada"
createGlobalAnnouncement: "Anuncio global creado"
createUserAnnouncement: "Anuncio de usuario creado"
updateGlobalAnnouncement: "Anuncio global actualizado"
updateUserAnnouncement: "Anuncio de usuario actualizado"
deleteGlobalAnnouncement: "Anuncio global eliminado"
deleteUserAnnouncement: "Anuncio de usuario eliminado"
resetPassword: "Resetear contraseña"
suspendRemoteInstance: "Instancia remota suspendida"
unsuspendRemoteInstance: "Suspensión de instancia remota retirada"
markSensitiveDriveFile: "Archivo marcado como sensible"
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
resolveAbuseReport: "Reporte resuelto"
createInvitation: "Generar invitación"

View File

@ -1114,6 +1114,7 @@ currentAnnouncements: "Pengumuman Saat Ini"
pastAnnouncements: "Pengumuman Terdahulu"
replies: "Balas"
renotes: "Renote"
dateAndTime: "Tanggal dan Waktu"
_initialAccountSetting:
accountCreated: "Akun kamu telah sukses dibuat!"
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
@ -2128,3 +2129,4 @@ _webhookSettings:
_moderationLogTypes:
suspend: "Tangguhkan"
resetPassword: "Atur ulang kata sandi"
createInvitation: "Buat kode undangan"

17
locales/index.d.ts vendored
View File

@ -1201,6 +1201,12 @@ export interface Locale {
"authentication": string;
"authenticationRequiredToContinue": string;
"dateAndTime": string;
"showRenotes": string;
"edited": string;
"notificationRecieveConfig": string;
"mutualFollow": string;
"fileAttachedOnly": string;
"showCatOnly": string;
"additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
@ -1213,8 +1219,6 @@ export interface Locale {
"step1_2": string;
"step1_3": string;
"step1_4": string;
"step1_5": string;
"step1_6": string;
};
"_announcement": {
"forExistingUsers": string;
@ -1705,9 +1709,8 @@ export interface Locale {
"_options": {
"gtlAvailable": string;
"ltlAvailable": string;
"mtlAvailable": string;
"ctlAvailable": string;
"canPublicNote": string;
"canEditNote": string;
"canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;
@ -2342,9 +2345,7 @@ export interface Locale {
"_timelines": {
"home": string;
"local": string;
"media": string;
"social": string;
"cat": string;
"global": string;
};
"_play": {
@ -2551,6 +2552,10 @@ export interface Locale {
"markSensitiveDriveFile": string;
"unmarkSensitiveDriveFile": string;
"resolveAbuseReport": string;
"createInvitation": string;
"createAd": string;
"deleteAd": string;
"updateAd": string;
};
"_abuse": {
"_resolver": {

View File

@ -130,8 +130,8 @@ unmarkAsSensitive: "Non segnare come esplicito "
enterFileName: "Nome del file"
mute: "Silenzia"
unmute: "Riattiva l'audio"
renoteMute: "Silenzia i Rinota"
renoteUnmute: "Non silenziare i Rinota"
renoteMute: "Silenzia le Rinota"
renoteUnmute: "Non silenziare le Rinota"
block: "Blocca"
unblock: "Sblocca"
suspend: "Sospensione"
@ -1005,7 +1005,7 @@ thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva"
thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
thisPostMayBeAnnoyingCancel: "Annulla"
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
collapseRenotes: "Comprimi i Rinota già letti"
collapseRenotes: "Comprimi le Rinota già viste"
internalServerError: "Errore interno del server"
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
copyErrorInfo: "Copia le informazioni sull'errore"
@ -1134,6 +1134,8 @@ notifyNotes: "Notifica nuove Note"
unnotifyNotes: "Interrompi le notifiche di nuove Note"
authentication: "Autenticazione"
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
dateAndTime: "Data e Ora"
showRenotes: "Leggi le Rinota"
_announcement:
forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -2187,6 +2189,8 @@ _webhookSettings:
reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano"
_moderationLogTypes:
createRole: "Ruolo creato"
deleteRole: "Ruolo eliminato"
updateRole: "Ruolo aggiornato"
assignRole: "Ruolo assegnato"
unassignRole: "Ruolo disassegnato"
@ -2210,3 +2214,5 @@ _moderationLogTypes:
unsuspendRemoteInstance: "Istanza remota riattivata"
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
resolveAbuseReport: "Segnalazione risolta"
createInvitation: "Genera codice di invito"

View File

@ -1198,6 +1198,12 @@ unnotifyNotes: "投稿の通知を解除"
authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時"
showRenotes: "リノートを表示"
edited: "編集済み"
notificationRecieveConfig: "通知の受信設定"
mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ"
showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか"
@ -1209,10 +1215,8 @@ _messaging:
_tlTutorial:
step1_1: '{icon} ホームタイムラインは、あなたがフォローしているアカウントの投稿を見られます。'
step1_2: '{icon} ローカルタイムラインでは、このサーバーにいるみんなの投稿を見られます。'
step1_3: '{icon} メディアタイムラインは、このサーバーにいるみんなの投稿のうち、メディア(写真、動画)を含む投稿のみを見ることができます。'
step1_4: '{icon} ソーシャルタイムラインでは、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。'
step1_5: '{icon} キャットタイムラインでは、ソーシャルタイムラインでキャットを有効にしたユーザーの投稿を見ることができます。'
step1_6: '{icon} グローバルタイムラインでは、このサーバーに接続されているすべてのサーバーからの投稿を見られます。'
step1_3: '{icon} ソーシャルタイムラインでは、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。'
step1_4: '{icon} グローバルタイムラインでは、このサーバーに接続されているすべてのサーバーからの投稿を見られます。'
_announcement:
forExistingUsers: "既存ユーザーのみ"
@ -1625,9 +1629,8 @@ _role:
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
mtlAvailable: "メディアタイムラインの閲覧"
ctlAvailable: "キャットタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
@ -2254,9 +2257,7 @@ _instanceCharts:
_timelines:
home: "ホーム"
local: "ローカル"
media: "メディア"
social: "ソーシャル"
cat: "キャット"
global: "グローバル"
_play:
@ -2461,6 +2462,10 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決"
createInvitation: "招待コードを作成"
createAd: "広告を作成"
deleteAd: "広告を削除"
updateAd: "広告を更新"
_abuse:
_resolver:

View File

@ -1119,6 +1119,10 @@ youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんや
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
replies: "返事"
renotes: "Renote"
loadReplies: "返信を見るで"
loadConversation: "会話を見るで"
verifiedLink: "このリンク先の所有者であることが確認されたで。"
authenticationRequiredToContinue: "続けるには認証をやってや。"
_announcement:
forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
@ -1147,6 +1151,11 @@ _serverRules:
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
_serverSettings:
iconUrl: "アイコン画像のURL"
appIconDescription: "{host}がアプリとして表示してるんやつをアイコンを指定すんで。"
appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。"
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。"
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromSub: "別のアカウントへエイリアスを作る"
@ -1783,6 +1792,7 @@ _2fa:
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
step3Title: "確認コードを入れてーや"
step3: "アプリに表示されているトークンを入力して終わりや。"
setupCompleted: "設定が完了したで。"
step4: "これからログインするときも、同じようにトークンを入力するんやで"
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
@ -1797,6 +1807,10 @@ _2fa:
renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?"
renewTOTPOk: "もっかい設定する"
renewTOTPCancel: "やめとく"
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。"
backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。"
backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。"
backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。"
_permissions:
"read:account": "アカウントの情報を見るで"
"write:account": "アカウントの情報を変更するで"
@ -2074,6 +2088,9 @@ _notification:
unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
achievementEarned: "実績を獲得しとるで"
checkNotificationBehavior: "通知の表示を確かめるで"
sendTestNotification: "テスト通知を送信するで"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
_types:
all: "すべて"
follow: "フォロー"
@ -2110,6 +2127,7 @@ _deck:
introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで"
_columns:
main: "メイン"
widgets: "ウィジェット"
@ -2147,3 +2165,4 @@ _webhookSettings:
_moderationLogTypes:
suspend: "凍結"
resetPassword: "パスワードをリセット"
createInvitation: "招待コードを作成"

View File

@ -1186,6 +1186,7 @@ youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있어요."
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주세요."
replies: "답글"
renotes: "리노트"
showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
@ -1195,10 +1196,8 @@ _messaging:
_tlTutorial:
step1_1: '{icon} 홈 타임라인은 내가 팔로우하고 있는 계정의 게시물을 볼 수 있어요.'
step1_2: '{icon} 로컬 타임라인은 이 서버의 모든 유저가 올린 게시물을 볼 수 있어요.'
step1_3: '{icon} 미디어 타임라인은 이 서버의 모든 유저가 올린 게시물 중, 미디어(사진, 영상)가 포함된 게시물만 볼 수 있어요.'
step1_4: '{icon} 소셜 타임라인은 홈 타임라인과 로컬 타임라인을 합친 것과 같아요.'
step1_5: '{icon} 고양이 타임라인에서는 소셜 타임라인에서 고양이를 활성화한 유저의 게시물을 볼 수 있어요.'
step1_6: '{icon} 글로벌 타임라인에서는 이 서버와 연결된 모든 서버의 게시물을 볼 수 있어요.'
step1_3: '{icon} 소셜 타임라인은 홈 타임라인과 로컬 타임라인을 합친 것과 같아요.'
step1_4: '{icon} 글로벌 타임라인에서는 이 서버와 연결된 모든 서버의 게시물을 볼 수 있어요.'
_announcement:
forExistingUsers: "기존 유저에게만 알리기"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시해요. 비활성화하면 게시 후에 가입한 유저에게도 표시해요."
@ -1589,8 +1588,6 @@ _role:
_options:
gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기"
mtlAvailable: "미디어 타임라인 보이기"
ctlAvailable: "고양이 타임라인 보이기"
canPublicNote: "공개 노트 허용"
canInvite: "서버 초대 코드 발행"
inviteLimit: "초대 한도"
@ -2173,9 +2170,7 @@ _instanceCharts:
_timelines:
home: "홈"
local: "로컬"
media: "미디어"
social: "소셜"
cat: "고양이"
global: "글로벌"
_play:
new: "Play 만들기"
@ -2341,6 +2336,7 @@ _webhookSettings:
_moderationLogTypes:
suspend: "정지"
resetPassword: "비밀번호 재설정"
createInvitation: "초대 코드 생성"
_abuse:
_resolver:
1hour: "1시간"

View File

@ -416,6 +416,9 @@ totp: "แอป Authenticator"
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
moderator: "ผู้ควบคุม"
moderation: "การกลั่นกรอง"
moderationNote: "โน้ตการกลั่นกรอง"
addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
moderationLogs: "บันทึกการกลั่นกรอง"
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
securityKey: "กุญแจความปลอดภัย"
@ -720,6 +723,7 @@ lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องต
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
highlightSensitiveMedia: "ไฮไลท์สื่อที่ละเอียดอ่อน"
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
notSet: "ไม่ได้ตั้งค่า"
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
@ -1036,6 +1040,7 @@ retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการ
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
reactionsDisplaySize: "รีแอคชั่นแสดงผลขนาด"
noteIdOrUrl: "โน้ต ID หรือ URL"
video: "วีดีโอ"
videos: "วีดีโอ"
@ -1114,11 +1119,23 @@ iHaveReadXCarefullyAndAgree: "ฉันได้อ่านข้อควา
dialog: "ไดอะล็อก"
icon: "ไอคอน"
forYou: "สำหรับคุณ"
currentAnnouncements: "ประกาศในปัจจุบัน"
pastAnnouncements: "ประกาศที่ผ่านมา"
youHaveUnreadAnnouncements: "มีการประกาศที่ยังไม่ได้อ่าน"
replies: "ตอบกลับ"
renotes: "รีโน้ต"
loadReplies: "แสดงการตอบกลับ"
loadConversation: "แสดงบทสนทนา"
pinnedList: "รายการที่ปักหมุดไว้แล้ว"
keepScreenOn: "เปิดหน้าจอไว้"
notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่"
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
authentication: "การตรวจสอบสิทธิ์"
dateAndTime: "เวลาประทับ"
showRenotes: "แสดงรีโน้ต"
edited: "แก้ไขแล้ว"
_announcement:
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
end: "ประกาศเก็บถาวร"
@ -1144,6 +1161,7 @@ _serverRules:
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
_serverSettings:
iconUrl: "ไอคอน URL"
shortName: "ชื่อย่อ"
_accountMigration:
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
@ -1438,6 +1456,7 @@ _role:
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canEditNote: "กำลังแก้ไขโน้ต"
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
inviteLimit: "จำกัดการเชิญ"
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
@ -2072,6 +2091,7 @@ _notification:
yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ"
youWereInvitedToGroup: "{userName} ได้เชิญคุณเข้ากลุ่ม"
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
newNote: "โพสต์ใหม่"
unreadAntennaNote: "เสาอากาศ {name}"
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
achievementEarned: "รับความสำเร็จ"
@ -2081,6 +2101,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้"
_types:
all: "ทั้งหมด"
note: "โน้ตใหม่"
follow: "กำลังติดตาม"
mention: "กล่าวถึง"
reply: "ตอบกลับ"
@ -2152,5 +2173,20 @@ _webhookSettings:
reaction: "เมื่อได้รับรีแอคชั่น"
mention: "เมื่อกำลังถูกกล่าวถึง"
_moderationLogTypes:
createRole: "สร้างบทบาทแล้ว"
deleteRole: "ลบบทบาทแล้ว"
updateRole: "อัปเดตบทบาทแล้ว"
assignRole: "ได้รับมอบหมายบทบาท"
unassignRole: "ถอดออกจากบทบาทแล้ว"
suspend: "ถูกระงับ"
unsuspend: "เลิกถูกระงับ"
addCustomEmoji: "เพิ่มอีโมจิที่กำหนดเองแล้ว"
updateCustomEmoji: "อัปเดตอีโมจิที่กำหนดเองแล้ว"
deleteCustomEmoji: "ลบอีโมจิที่กำหนดเองออกแล้ว"
updateServerSettings: "อัปเดตการตั้งค่าเซิร์ฟเวอร์แล้ว"
updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว"
deleteDriveFile: "ลบไฟล์ออกแล้ว"
deleteNote: "ลบโน้ตออกแล้ว"
resetPassword: "รีเซ็ตรหัสผ่าน"
resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
createInvitation: "สร้างคำเชิญ"

View File

@ -1134,6 +1134,9 @@ notifyNotes: "打开发帖通知"
unnotifyNotes: "关闭发帖通知"
authentication: "验证"
authenticationRequiredToContinue: "要继续,请先进行验证"
dateAndTime: "日期和时间"
showRenotes: "显示转帖"
edited: "已编辑"
_announcement:
forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@ -1464,6 +1467,7 @@ _role:
gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖"
canEditNote: "编辑帖子"
canInvite: "发放服务器邀请码"
inviteLimit: "可发行邀请码的数量"
inviteLimitCycle: "邀请码的发行间隔"
@ -2187,6 +2191,8 @@ _webhookSettings:
reaction: "被回应时"
mention: "被提及时"
_moderationLogTypes:
createRole: "创建角色"
deleteRole: "删除角色"
updateRole: "更新角色"
assignRole: "分配角色"
unassignRole: "取消分配角色"
@ -2208,3 +2214,5 @@ _moderationLogTypes:
resetPassword: "重置密码"
markSensitiveDriveFile: "标记网盘文件为敏感媒体"
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
resolveAbuseReport: "处理举报"
createInvitation: "发行邀请码"

View File

@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消"
noThankYou: "現在不要"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉發"
renotedBy: "{user} 轉發"
noNotes: "無貼文"
noNotifications: "沒有通知"
instance: "伺服器"
@ -56,7 +56,7 @@ copyRSS: "複製RSS"
copyUsername: "複製使用者名稱"
copyUserId: "複製使用者 ID"
copyNoteId: "複製貼文 ID"
copyFileId: "複製檔案ID"
copyFileId: "複製檔案 ID"
copyFolderId: "複製資料夾ID"
copyProfileUrl: "複製個人資料網址"
searchUser: "搜尋使用者"
@ -75,9 +75,9 @@ import: "匯入"
export: "匯出"
files: "檔案"
download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此檔案的貼文也會跟著被刪除。"
unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端裡。"
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端硬碟裡。"
importRequested: "已請求匯入。這可能會花一點時間。"
lists: "清單"
noLists: "你沒有任何清單"
@ -156,16 +156,16 @@ emojiUrl: "表情符號 URL"
addEmoji: "新增表情符號"
settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但為了產生圖片的縮圖並保護使用者的隱私,建議將 default.yml 的 proxyRemoteFiles 設為 true。"
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私,。"
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
flagAsBot: "此使用者是機器人"
flagAsBotDescription: "標記本帳戶由程式控制,防止其他程式與本帳戶產生無限互動的行為。"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Misskey內部系統將本帳戶識別為機器人"
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
flagShowTimelineRepliesDescription: "啟用,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
addAccount: "新增帳戶"
reloadAccountsList: "更新帳戶清單的資訊"
@ -184,7 +184,7 @@ host: "主機"
selectUser: "選取使用者"
recipient: "收件人"
annotation: "註解"
federation: "聯邦宇宙"
federation: "站台聯邦"
instances: "伺服器"
registeredAt: "初次觀測"
latestRequestReceivedAt: "上次收到的請求"
@ -585,7 +585,7 @@ tokenRevokedDescription: "登入權杖失效,請重新登入。"
accountDeleted: "帳戶已被刪除"
accountDeletedDescription: "這個帳戶已被刪除。"
menu: "選單"
divider: "分線"
divider: "分線"
addItem: "新增項目"
rearrange: "排序方式"
relays: "中繼"
@ -594,7 +594,7 @@ inboxUrl: "收件夾URL"
addedRelays: "已加入的中繼"
serviceworkerInfo: "您需要啟用推送通知。"
deletedNote: "已刪除的貼文"
invisibleNote: "隱藏的貼文"
invisibleNote: "私密的貼文"
enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性"
poll: "投票"
@ -1135,6 +1135,7 @@ unnotifyNotes: "關閉貼文通知"
authentication: "驗證"
authenticationRequiredToContinue: "請於繼續前完成驗證"
dateAndTime: "日期與時間"
showRenotes: "顯示轉發貼文"
_announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1465,6 +1466,7 @@ _role:
gtlAvailable: "瀏覽全域時間軸"
ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文"
canEditNote: "允許編輯貼文"
canInvite: "發行實例邀請碼"
inviteLimit: "可建立邀請碼的數量"
inviteLimitCycle: "邀請碼的發放間隔"
@ -1656,8 +1658,8 @@ _mfm:
plainDescription: "停用全部的內部語法。"
_instanceTicker:
none: "隱藏"
remote: "向遠端使用者顯示"
always: "總是顯示"
remote: "只顯示遠端使用者"
always: "一律顯示"
_serverDisconnectedBehavior:
reload: "自動重載"
dialog: "彈出式警告"
@ -1690,7 +1692,7 @@ _wordMute:
mutedNotes: "已靜音的貼文"
_instanceMute:
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
instanceMuteDescription2: "換行以分隔"
instanceMuteDescription2: "設定時以換行進行分隔"
title: "將隱藏被設定的實例貼文。"
heading: "將實例靜音"
_theme:
@ -1743,7 +1745,7 @@ _theme:
mentionMe: "提到了我"
renote: "轉發貼文"
modalBg: "對話框背景"
divider: "分線"
divider: "分線"
scrollbarHandle: "捲動條"
scrollbarHandleHover: "捲動條(懸浮)"
dateLabelFg: "日期標籤文字"
@ -2213,3 +2215,5 @@ _moderationLogTypes:
unsuspendRemoteInstance: "解除封鎖遠端伺服器"
markSensitiveDriveFile: "標記為敏感檔案"
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
resolveAbuseReport: "解決檢舉"
createInvitation: "建立邀請碼"

View File

@ -1,13 +1,13 @@
{
"name": "cherrypick",
"version": "4.3.0",
"basedMisskeyVersion": "2023.9.1",
"version": "4.3.1",
"basedMisskeyVersion": "2023.9.2",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/kokonect-link/cherrypick.git"
},
"packageManager": "pnpm@8.7.6",
"packageManager": "pnpm@8.8.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -49,15 +49,15 @@
"execa": "8.0.1",
"cssnano": "6.0.1",
"js-yaml": "4.1.0",
"postcss": "8.4.30",
"postcss": "8.4.31",
"terser": "5.20.0",
"typescript": "5.2.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"cross-env": "7.0.3",
"cypress": "13.2.0",
"cypress": "13.3.0",
"eslint": "8.50.0",
"start-server-and-test": "2.0.1"
},

View File

@ -0,0 +1,11 @@
export class NoteUpdatedAt1695901659683 {
name = 'NoteUpdatedAt1695901659683'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
}
}

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NotificationRecieveConfig1695944637565 {
name = 'NotificationRecieveConfig1695944637565'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "notificationRecieveConfig" jsonb NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "notificationRecieveConfig"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "public"."user_profile_notificationrecieveconfig_enum" array NOT NULL DEFAULT '{}'`);
}
}

View File

@ -39,7 +39,7 @@
"@swc/core-win32-x64-msvc": "1.3.56",
"@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.4.0",
"bufferutil": "^4.0.7",
"bufferutil": "4.0.7",
"slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10",
"slacc-darwin-arm64": "0.0.10",
@ -53,7 +53,7 @@
"slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "^6.0.3"
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.412.0",
@ -68,7 +68,7 @@
"@fastify/cors": "8.4.0",
"@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.3",
"@fastify/multipart": "8.0.0",
"@fastify/static": "6.11.2",
"@fastify/view": "8.2.0",
"@google-cloud/logging": "^10.5.0",
@ -77,10 +77,10 @@
"@nestjs/core": "10.2.6",
"@nestjs/testing": "10.2.6",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.1.1",
"@simplewebauthn/server": "8.2.0",
"@sinonjs/fake-timers": "11.1.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.87",
"@swc/core": "1.3.90",
"@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
@ -120,7 +120,7 @@
"json5": "2.2.3",
"jsonld": "8.3.1",
"jsrsasign": "10.8.6",
"meilisearch": "0.34.2",
"meilisearch": "0.35.0",
"microformats-parser": "1.5.2",
"mime-types": "2.1.35",
"ms": "3.0.0-canary.1",
@ -159,7 +159,7 @@
"stringz": "2.1.0",
"strip-ansi": "^7.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.8",
"systeminformation": "5.21.9",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
@ -191,33 +191,33 @@
"@types/jsdom": "21.1.3",
"@types/jsonld": "1.5.10",
"@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.1",
"@types/ms": "0.7.31",
"@types/node": "20.6.4",
"@types/mime-types": "2.1.2",
"@types/ms": "0.7.32",
"@types/node": "20.7.1",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0",
"@types/pg": "8.10.2",
"@types/pug": "2.0.6",
"@types/pg": "8.10.3",
"@types/pug": "2.0.7",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.2",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0",
"@types/semver": "7.5.2",
"@types/rename": "1.0.5",
"@types/sanitize-html": "2.9.1",
"@types/semver": "7.5.3",
"@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.4",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/simple-oauth2": "5.0.5",
"@types/sinonjs__fake-timers": "8.1.3",
"@types/tinycolor2": "1.4.4",
"@types/tmp": "0.2.4",
"@types/vary": "1.1.0",
"@types/web-push": "3.6.0",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@types/vary": "1.1.1",
"@types/web-push": "3.6.1",
"@types/ws": "8.5.6",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.50.0",

View File

@ -66,6 +66,7 @@ export async function masterMain() {
showNodejsVersion();
config = loadConfigBoot();
//await connectDb();
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true);
process.exit(1);

View File

@ -97,6 +97,7 @@ type Source = {
perChannelMaxNoteCacheCount?: number;
perUserNotificationsMaxCount?: number;
deactivateAntennaThreshold?: number;
pidFile: string;
};
export type Config = {
@ -173,6 +174,7 @@ export type Config = {
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
pidFile: string;
};
const _filename = fileURLToPath(import.meta.url);
@ -268,6 +270,7 @@ export function loadConfig(): Config {
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
pidFile: config.pidFile,
};
}

View File

@ -15,7 +15,7 @@ import { DI } from '@/di-symbols.js';
import type { AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -53,7 +53,7 @@ export class AntennaService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'antennaCreated':
this.antennas.push({

View File

@ -11,7 +11,7 @@ import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { FlashToken } from '@/misc/flash-token.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@ -169,7 +169,7 @@ export class CacheService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {

View File

@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;

View File

@ -5,31 +5,301 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiChannel } from '@/models/Channel.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiUserList } from '@/models/UserList.js';
import type { MiUserGroup } from '@/models/UserGroup.js';
import type { MiAntenna } from '@/models/Antenna.js';
import type {
StreamChannels,
AdminStreamTypes,
AntennaStreamTypes,
BroadcastTypes,
DriveStreamTypes,
GroupMessagingStreamTypes,
InternalStreamTypes,
MainStreamTypes,
MessagingIndexStreamTypes,
MessagingStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
RoleTimelineStreamTypes,
} from '@/server/api/stream/types.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
import type { MiUserList } from '@/models/UserList.js';
import type { MiMessagingMessage } from '@/models/MessagingMessage.js';
import type { MiUserGroup } from '@/models/UserGroup.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { MiRole } from '@/models/_.js';
import { Serialized } from '@/types.js';
import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events';
//#region Stream type-body definitions
export interface BroadcastTypes {
emojiAdded: {
emoji: Packed<'EmojiDetailed'>;
};
emojiUpdated: {
emojis: Packed<'EmojiDetailed'>[];
};
emojiDeleted: {
emojis: {
id?: string;
name: string;
[other: string]: any;
}[];
};
announcementCreated: {
announcement: Packed<'Announcement'>;
};
}
export interface MainEventTypes {
notification: Packed<'Notification'>;
mention: Packed<'Note'>;
reply: Packed<'Note'>;
renote: Packed<'Note'>;
follow: Packed<'UserDetailedNotMe'>;
followed: Packed<'User'>;
unfollow: Packed<'User'>;
meUpdated: Packed<'User'>;
pageEvent: {
pageId: MiPage['id'];
event: string;
var: any;
userId: MiUser['id'];
user: Packed<'User'>;
};
urlUploadFinished: {
marker?: string | null;
file: Packed<'DriveFile'>;
};
readAllNotifications: undefined;
unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: MiNote['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllMessagingMessages: undefined;
messagingMessage: Packed<'MessagingMessage'>;
unreadMessagingMessage: Packed<'MessagingMessage'>;
readAllAntennas: undefined;
unreadAntenna: MiAntenna;
readAllAnnouncements: undefined;
myTokenRegenerated: undefined;
signin: MiSignin;
registryUpdated: {
scope?: string[];
key: string;
value: any | null;
};
driveFileCreated: Packed<'DriveFile'>;
readAntenna: MiAntenna;
receiveFollowRequest: Packed<'User'>;
announcementCreated: {
announcement: Packed<'Announcement'>;
};
}
export interface DriveEventTypes {
fileCreated: Packed<'DriveFile'>;
fileDeleted: MiDriveFile['id'];
fileUpdated: Packed<'DriveFile'>;
folderCreated: Packed<'DriveFolder'>;
folderDeleted: MiDriveFolder['id'];
folderUpdated: Packed<'DriveFolder'>;
}
export interface NoteEventTypes {
pollVoted: {
choice: number;
userId: MiUser['id'];
};
deleted: {
deletedAt: Date;
};
updated: {
cw: string | null;
text: string;
};
reacted: {
reaction: string;
emoji?: {
name: string;
url: string;
} | null;
userId: MiUser['id'];
};
unreacted: {
reaction: string;
userId: MiUser['id'];
};
}
type NoteStreamEventTypes = {
[key in keyof NoteEventTypes]: {
id: MiNote['id'];
body: NoteEventTypes[key];
};
};
export interface ChannelEventTypes {
typing: MiUser['id'];
}
export interface UserListEventTypes {
userAdded: Packed<'User'>;
userRemoved: Packed<'User'>;
}
export interface AntennaEventTypes {
note: MiNote;
}
export interface MessagingEventTypes {
read: MiMessagingMessage['id'][];
typing: MiUser['id'];
message: Packed<'MessagingMessage'>;
deleted: MiMessagingMessage['id'];
}
export interface GroupMessagingEventTypes {
read: {
ids: MiMessagingMessage['id'][];
userId: MiUser['id'];
};
typing: MiUser['id'];
message: Packed<'MessagingMessage'>;
deleted: MiMessagingMessage['id'];
}
export interface MessagingIndexEventTypes {
read: MiMessagingMessage['id'][];
message: Packed<'MessagingMessage'>;
}
export interface RoleTimelineEventTypes {
note: Packed<'Note'>;
}
export interface AdminEventTypes {
newAbuseUserReport: {
id: MiAbuseUserReport['id'];
targetUserId: MiUser['id'],
reporterId: MiUser['id'],
comment: string;
};
}
//#endregion
// 辞書(interface or type)から{ type, body }ユニオンを定義
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
// VS Codeの展開を防止するためにEvents型を定義
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
type EventUnionFromDictionary<
T extends object,
U = Events<T>
> = U[keyof U];
type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
remoteUserUpdated: { id: MiUser['id']; };
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
policiesUpdated: MiRole['policies'];
roleCreated: MiRole;
roleDeleted: MiRole;
roleUpdated: MiRole;
userRoleAssigned: MiRoleAssignment;
userRoleUnassigned: MiRoleAssignment;
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
metaUpdated: MiMeta;
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
updateUserProfile: MiUserProfile;
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
}
// name/messages(spec) pairs dictionary
export type GlobalEvents = {
internal: {
name: 'internal';
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
};
broadcast: {
name: 'broadcast';
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
};
main: {
name: `mainStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
};
drive: {
name: `driveStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
};
note: {
name: `noteStream:${MiNote['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
};
channel: {
name: `channelStream:${MiChannel['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ChannelEventTypes>>;
};
userList: {
name: `userListStream:${MiUserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
};
messaging: {
name: `messagingStream:${MiUser['id']}-${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingEventTypes>>;
};
groupMessaging: {
name: `messagingStream:${MiUserGroup['id']}`;
payload: EventUnionFromDictionary<SerializedAll<GroupMessagingEventTypes>>;
};
messagingIndex: {
name: `messagingIndexStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingIndexEventTypes>>;
};
roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
};
antenna: {
name: `antennaStream:${MiAntenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
};
admin: {
name: `adminStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
};
notes: {
name: 'notesStream';
payload: Serialized<Packed<'Note'>>;
};
};
// API event definitions
// ストリームごとのEmitterの辞書を用意
type EventEmitterDictionary = { [x in keyof GlobalEvents]: Emitter.default<EventEmitter, { [y in GlobalEvents[x]['name']]: (e: GlobalEvents[x]['payload']) => void }> };
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof GlobalEvents]>;
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
// provide stream channels union
export type StreamChannels = GlobalEvents[keyof GlobalEvents]['name'];
@Injectable()
export class GlobalEventService {
@ -55,7 +325,7 @@ export class GlobalEventService {
}
@bindThis
public publishInternalEvent<K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void {
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
@ -65,17 +335,17 @@ export class GlobalEventService {
}
@bindThis
public publishMainStream<K extends keyof MainStreamTypes>(userId: MiUser['id'], type: K, value?: MainStreamTypes[K]): void {
public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishDriveStream<K extends keyof DriveStreamTypes>(userId: MiUser['id'], type: K, value?: DriveStreamTypes[K]): void {
public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishNoteStream<K extends keyof NoteStreamTypes>(noteId: MiNote['id'], type: K, value?: NoteStreamTypes[K]): void {
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
body: value,
@ -83,32 +353,32 @@ export class GlobalEventService {
}
@bindThis
public publishUserListStream<K extends keyof UserListStreamTypes>(listId: MiUserList['id'], type: K, value?: UserListStreamTypes[K]): void {
public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishAntennaStream<K extends keyof AntennaStreamTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaStreamTypes[K]): void {
public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishMessagingStream<K extends keyof MessagingStreamTypes>(userId: MiUser['id'], otherpartyId: MiUser['id'], type: K, value?: MessagingStreamTypes[K]): void {
public publishMessagingStream<K extends keyof MessagingEventTypes>(userId: MiUser['id'], otherpartyId: MiUser['id'], type: K, value?: MessagingEventTypes[K]): void {
this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishGroupMessagingStream<K extends keyof GroupMessagingStreamTypes>(groupId: MiUserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void {
public publishGroupMessagingStream<K extends keyof GroupMessagingEventTypes>(groupId: MiUserGroup['id'], type: K, value?: GroupMessagingEventTypes[K]): void {
this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishMessagingIndexStream<K extends keyof MessagingIndexStreamTypes>(userId: MiUser['id'], type: K, value?: MessagingIndexStreamTypes[K]): void {
public publishMessagingIndexStream<K extends keyof MessagingIndexEventTypes>(userId: MiUser['id'], type: K, value?: MessagingIndexEventTypes[K]): void {
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
}
@ -118,7 +388,7 @@ export class GlobalEventService {
}
@bindThis
public publishAdminStream<K extends keyof AdminStreamTypes>(userId: MiUser['id'], type: K, value?: AdminStreamTypes[K]): void {
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
}

View File

@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
import { MiMeta } from '@/models/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -46,7 +46,7 @@ export class MetaService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'metaUpdated': {
this.cache = body;

View File

@ -111,9 +111,8 @@ class NotificationManager {
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
this.notificationService.createNotification(x.target, x.reason, {
notifierId: this.notifier.id,
noteId: this.note.id,
});
}, this.notifier.id);
}
}
}
@ -538,9 +537,8 @@ export class NoteCreateService implements OnApplicationShutdown {
}).then(followings => {
for (const following of followings) {
this.notificationService.createNotification(following.followerId, 'note', {
notifierId: user.id,
noteId: note.id,
});
}, user.id);
}
});
}

View File

@ -18,6 +18,7 @@ import { NotificationEntityService } from '@/core/entities/NotificationEntitySer
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { UserListService } from '@/core/UserListService.js';
@Injectable()
export class NotificationService implements OnApplicationShutdown {
@ -38,6 +39,7 @@ export class NotificationService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
private userListService: UserListService,
) {
}
@ -74,27 +76,56 @@ export class NotificationService implements OnApplicationShutdown {
public async createNotification(
notifieeId: MiUser['id'],
type: MiNotification['type'],
data: Partial<MiNotification>,
data: Omit<Partial<MiNotification>, 'notifierId'>,
notifierId?: MiUser['id'] | null,
): Promise<MiNotification | null> {
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
const recieveConfig = profile.notificationRecieveConfig[type];
if (recieveConfig?.type === 'never') {
return null;
}
if (data.notifierId) {
if (notifieeId === data.notifierId) {
if (notifierId) {
if (notifieeId === notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(data.notifierId)) {
if (mutings.has(notifierId)) {
return null;
}
if (recieveConfig?.type === 'following') {
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
if (!isFollowing) {
return null;
}
} else if (recieveConfig?.type === 'follower') {
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
if (!isFollower) {
return null;
}
} else if (recieveConfig?.type === 'mutualFollow') {
const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
]);
if (!isFollowing && !isFollower) {
return null;
}
} else if (recieveConfig?.type === 'list') {
const isMember = await this.userListService.membersCache.fetch(recieveConfig.userListId).then(members => members.has(notifierId));
if (!isMember) {
return null;
}
}
}
const notification = {
id: this.idService.genId(),
createdAt: new Date(),
type: type,
notifierId: notifierId,
...data,
} as MiNotification;
@ -117,8 +148,8 @@ export class NotificationService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;

View File

@ -219,10 +219,9 @@ export class ReactionService {
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) {
this.notificationService.createNotification(note.userId, 'reaction', {
notifierId: user.id,
noteId: note.id,
reaction: reaction,
});
}, user.id);
}
//#region 配信

View File

@ -15,7 +15,7 @@ import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -25,9 +25,9 @@ import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
mtlAvailable: boolean;
ctlAvailable: boolean;
canPublicNote: boolean;
canEditNote: boolean;
canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
@ -51,9 +51,9 @@ export type RolePolicies = {
export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
mtlAvailable: true,
ctlAvailable: true,
canPublicNote: true,
canEditNote: true,
canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
@ -118,7 +118,7 @@ export class RoleService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'roleCreated': {
const cached = this.rolesCache.get();
@ -297,9 +297,9 @@ export class RoleService implements OnApplicationShutdown {
return {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
mtlAvailable: calc('mtlAvailable', vs => vs.some(v => v === true)),
ctlAvailable: calc('ctlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

View File

@ -230,8 +230,7 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
notifierId: followee.id,
});
}, followee.id);
}
if (alreadyFollowed) return;
@ -304,8 +303,7 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(followee.id, 'follow', {
notifierId: follower.id,
});
}, follower.id);
}
}
@ -488,9 +486,8 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
notifierId: follower.id,
followRequestId: followRequest.id,
});
}, follower.id);
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {

View File

@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { UserListJoiningsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js';
@ -16,12 +17,22 @@ import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
@Injectable()
export class UserListService {
export class UserListService implements OnApplicationShutdown {
public static TooManyUsersError = class extends Error {};
public membersCache: RedisKVCache<Set<string>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
@ -32,10 +43,48 @@ export class UserListService {
private proxyAccountService: ProxyAccountService,
private queueService: QueueService,
) {
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async push(target: MiUser, list: MiUserList, me: MiUser) {
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'userListMemberAdded': {
const { userListId, memberId } = body;
const members = await this.membersCache.get(userListId);
if (members) {
members.add(memberId);
}
break;
}
case 'userListMemberRemoved': {
const { userListId, memberId } = body;
const members = await this.membersCache.get(userListId);
if (members) {
members.delete(memberId);
}
break;
}
default:
break;
}
}
}
@bindThis
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id,
});
@ -50,6 +99,7 @@ export class UserListService {
userListId: list.id,
} as MiUserListJoining);
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
@ -60,4 +110,26 @@ export class UserListService {
}
}
}
@bindThis
public async removeMember(target: MiUser, list: MiUserList) {
await this.userListJoiningsRepository.delete({
userId: target.id,
userListId: list.id,
});
this.globalEventService.publishInternalEvent('userListMemberRemoved', { userListId: list.id, memberId: target.id });
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.membersCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@ -9,7 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -45,7 +45,7 @@ export class WebhookService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated':
if (body.active) {

View File

@ -322,6 +322,7 @@ export class NoteEntityService implements OnModuleInit {
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
detail: false,

View File

@ -490,7 +490,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
notificationRecieveConfig: profile!.notificationRecieveConfig,
emailNotificationTypes: profile!.emailNotificationTypes,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,

View File

@ -24,6 +24,11 @@ export class MiNote {
})
public createdAt: Date;
@Column('timestamp with time zone', {
default: null,
})
public updatedAt: Date | null;
@Index()
@Column({
...id(),

View File

@ -8,6 +8,7 @@ import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/ty
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
import { MiUserList } from './UserList.js';
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
@ -222,16 +223,25 @@ export class MiUserProfile {
})
public mutedInstances: string[];
@Column('enum', {
enum: [
...notificationTypes,
// マイグレーションで削除が困難なので古いenumは残しておく
...obsoleteNotificationTypes,
],
array: true,
default: [],
@Column('jsonb', {
default: {},
})
public mutingNotificationTypes: typeof notificationTypes[number][];
public notificationRecieveConfig: {
[notificationType in typeof notificationTypes[number]]?: {
type: 'all';
} | {
type: 'never';
} | {
type: 'following';
} | {
type: 'follower';
} | {
type: 'mutualFollow';
} | {
type: 'list';
userListId: MiUserList['id'];
};
};
@Column('varchar', {
length: 32, array: true, default: '{}',

View File

@ -17,6 +17,11 @@ export const packedNoteSchema = {
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
deletedAt: {
type: 'string',
optional: true, nullable: true,

View File

@ -391,13 +391,9 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
},
},
mutingNotificationTypes: {
type: 'array',
nullable: true, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
notificationRecieveConfig: {
type: 'object',
nullable: false, optional: false,
},
emailNotificationTypes: {
type: 'array',

View File

@ -101,7 +101,7 @@ export class ImportUserListsProcessorService {
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.push(target, list!, user);
this.userListService.addMember(target, list!, user);
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}

View File

@ -111,8 +111,6 @@ export class NodeinfoServerService {
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
disableLocalTimeline: !basePolicies.ltlAvailable,
disableMediaTimeline: !basePolicies.mtlAvailable,
disableCatTimeline: !basePolicies.ctlAvailable,
disableGlobalTimeline: !basePolicies.gtlAvailable,
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,

View File

@ -35,8 +35,6 @@ import { HashtagChannelService } from './api/stream/channels/hashtag.js';
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
import { MediaTimelineChannelService } from './api/stream/channels/media-timeline.js';
import { CatTimelineChannelService } from './api/stream/channels/cat-timeline.js';
import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js';
import { MessagingChannelService } from './api/stream/channels/messaging.js';
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
@ -84,8 +82,6 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
HomeTimelineChannelService,
HybridTimelineChannelService,
LocalTimelineChannelService,
MediaTimelineChannelService,
CatTimelineChannelService,
MessagingIndexChannelService,
MessagingChannelService,
QueueStatsChannelService,

View File

@ -272,14 +272,13 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mediaTimeline from './endpoints/notes/media-timeline.js';
import * as ep___notes_catTimeline from './endpoints/notes/cat-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
@ -650,14 +649,13 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
const $notes_mediaTimeline: Provider = { provide: 'ep:notes/media-timeline', useClass: ep___notes_mediaTimeline.default };
const $notes_catTimeline: Provider = { provide: 'ep:notes/cat-timeline', useClass: ep___notes_catTimeline.default };
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
@ -1033,14 +1031,13 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
$notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mediaTimeline,
$notes_catTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,
@ -1408,14 +1405,13 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
$notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mediaTimeline,
$notes_catTimeline,
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,

View File

@ -271,14 +271,13 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mediaTimeline from './endpoints/notes/media-timeline.js';
import * as ep___notes_catTimeline from './endpoints/notes/cat-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
@ -647,14 +646,13 @@ const eps = [
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/delete', ep___notes_delete],
['notes/update', ep___notes_update],
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],
['notes/global-timeline', ep___notes_globalTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/media-timeline', ep___notes_mediaTimeline],
['notes/cat-timeline', ep___notes_catTimeline],
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],
['notes/polls/vote', ep___notes_polls_vote],

View File

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AdsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private adsRepository: AdsRepository,
private idService: IdService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.adsRepository.insert({
const ad = await this.adsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
@ -53,7 +55,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
this.moderationLogService.log(me, 'createAd', {
adId: ad.id,
ad: ad,
});
return ad;
});
}
}

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AdsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const ad = await this.adsRepository.findOneBy({ id: ps.id });
@ -44,6 +47,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
await this.adsRepository.delete(ad.id);
this.moderationLogService.log(me, 'deleteAd', {
adId: ad.id,
ad: ad,
});
});
}
}

View File

@ -22,6 +22,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
publishing: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -36,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
if (ps.publishing) {
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
}
const ads = await query.limit(ps.limit).getMany();
return ads;

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AdsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const ad = await this.adsRepository.findOneBy({ id: ps.id });
@ -63,6 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
});
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
this.moderationLogService.log(me, 'updateAd', {
adId: ad.id,
before: ad,
after: updatedAd,
});
});
}
}

View File

@ -10,6 +10,7 @@ import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { generateInviteCode } from '@/misc/generate-invite-code.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -60,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private inviteCodeEntityService: InviteCodeEntityService,
private idService: IdService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
@ -78,6 +80,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const tickets = await Promise.all(ticketsPromises);
this.moderationLogService.log(me, 'createInvitation', {
invitations: tickets,
});
return await this.inviteCodeEntityService.packMany(tickets, me);
});
}

View File

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,
notificationRecieveConfig: profile.notificationRecieveConfig,
isModerator: isModerator,
isSilenced: isSilenced,
isSuspended: user.isSuspended,

View File

@ -165,9 +165,7 @@ export const paramDef = {
mutedInstances: { type: 'array', items: {
type: 'string',
} },
mutingNotificationTypes: { type: 'array', items: {
type: 'string', enum: notificationTypes,
} },
notificationRecieveConfig: { type: 'object' },
emailNotificationTypes: { type: 'array', items: {
type: 'string',
} },
@ -248,7 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;

View File

@ -1,152 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
errors: {
ctlDisabled: {
message: 'Cat timeline has been disabled.',
code: 'CTL_DISABLED',
id: '620763f4-f621-4533-ab33-0577a1a3c342',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.ctlAvailable) {
throw new ApiError(meta.errors.ctlDisabled);
}
//#region Construct query
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere(new Brackets(qb => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}))
.andWhere('(select "isCat" from "user" where id = note."userId")')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
});
}
}

View File

@ -34,10 +34,11 @@ describe('api:notes/create', () => {
.toBe(VALID);
});
test('null post', () => {
expect(v({ text: null }))
.toBe(INVALID);
});
// TODO
//test('null post', () => {
// expect(v({ text: null }))
// .toBe(INVALID);
//});
test('0 characters post', () => {
expect(v({ text: '' }))

View File

@ -119,7 +119,7 @@ export const paramDef = {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false,
nullable: true,
},
fileIds: {
type: 'array',

View File

@ -4,6 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
@ -40,6 +41,8 @@ export const paramDef = {
properties: {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -88,6 +91,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -52,6 +52,8 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -137,6 +139,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -42,6 +42,8 @@ export const paramDef = {
properties: {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
@ -110,6 +112,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -1,131 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
errors: {
mtlDisabled: {
message: 'Media timeline has been disabled.',
code: 'MTL_DISABLED',
id: '45a6eb02-7695-4393-b023-dd4be9aaaefd',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withReplies: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
excludeNsfw: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!policies.mtlAvailable) {
throw new ApiError(meta.errors.mtlDisabled);
}
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.andWhere('note.fileIds != \'{}\'')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.fileType != null) {
query.andWhere('note.fileIds != \'{}\'');
query.andWhere(new Brackets(qb => {
for (const type of ps.fileType!) {
const i = ps.fileType!.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
}
}));
if (ps.excludeNsfw) {
query.andWhere('note.cw IS NULL');
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(timeline, me);
});
}
}

View File

@ -42,6 +42,8 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -126,6 +128,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canEditNote',
kind: 'write:notes',
limit: {
duration: ms('1hour'),
max: 10,
minInterval: ms('1sec'),
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false,
},
cw: { type: 'string', nullable: true, maxLength: 100 },
},
required: ['noteId', 'text', 'cw'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private getterService: GetterService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (note.userId !== me.id) {
throw new ApiError(meta.errors.noSuchNote);
}
await this.notesRepository.update({ id: note.id }, {
updatedAt: new Date(),
cw: ps.cw,
text: ps.text,
});
this.globalEventService.publishNoteStream(note.id, 'updated', {
cw: ps.cw,
text: ps.text,
});
});
}
}

View File

@ -49,11 +49,14 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withCats: { type: 'boolean', default: false },
},
required: ['listId'],
} as const;
@ -130,9 +133,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}
if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
try {
await this.userListService.push(currentUser, userList, me);
await this.userListService.addMember(currentUser, userList, me);
} catch (err) {
if (err instanceof UserListService.TooManyUsersError) {
throw new ApiError(meta.errors.tooManyUsers);

View File

@ -4,12 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
import type { UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { UserListService } from '@/core/UserListService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -53,12 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
private userEntityService: UserEntityService,
private userListService: UserListService,
private getterService: GetterService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the list
@ -77,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
// Pull the user
await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id });
this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user));
await this.userListService.removeMember(user, userList);
});
}
}

View File

@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
try {
await this.userListService.push(user, userList, me);
await this.userListService.addMember(user, userList, me);
} catch (err) {
if (err instanceof UserListService.TooManyUsersError) {
throw new ApiError(meta.errors.tooManyUsers);

View File

@ -41,7 +41,9 @@ export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
includeReplies: { type: 'boolean', default: true },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withCats: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -114,10 +116,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (!ps.includeReplies) {
if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :userId', { userId: user.id });
@ -128,6 +140,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();

View File

@ -7,8 +7,6 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './channels/local-timeline.js';
import { MediaTimelineChannelService } from './channels/media-timeline.js';
import { CatTimelineChannelService } from './channels/cat-timeline.js';
import { HomeTimelineChannelService } from './channels/home-timeline.js';
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
import { MainChannelService } from './channels/main.js';
@ -30,8 +28,6 @@ export class ChannelsService {
private mainChannelService: MainChannelService,
private homeTimelineChannelService: HomeTimelineChannelService,
private localTimelineChannelService: LocalTimelineChannelService,
private mediaTimelineChannelService: MediaTimelineChannelService,
private catTimelineChannelService: CatTimelineChannelService,
private hybridTimelineChannelService: HybridTimelineChannelService,
private globalTimelineChannelService: GlobalTimelineChannelService,
private userListChannelService: UserListChannelService,
@ -54,9 +50,7 @@ export class ChannelsService {
case 'main': return this.mainChannelService;
case 'homeTimeline': return this.homeTimelineChannelService;
case 'localTimeline': return this.localTimelineChannelService;
case 'mediaTimeline': return this.mediaTimelineChannelService;
case 'hybridTimeline': return this.hybridTimelineChannelService;
case 'catTimeline': return this.catTimelineChannelService;
case 'globalTimeline': return this.globalTimelineChannelService;
case 'userList': return this.userListChannelService;
case 'hashtag': return this.hashtagChannelService;

View File

@ -13,10 +13,10 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiUserProfile } from '@/models/_.js';
import type { MiUserGroup } from '@/models/UserGroup.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
import type { StreamEventEmitter, StreamMessages } from './types.js';
/**
* Main stream connection
@ -128,7 +128,7 @@ export default class Connection {
}
@bindThis
private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) {
this.sendMessageToWs(data.type, data.body);
}
@ -202,7 +202,7 @@ export default class Connection {
}
@bindThis
private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,

View File

@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common';
import { isUserRelated } from '@/misc/is-user-related.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
import type { StreamMessages } from '../types.js';
class AntennaChannel extends Channel {
public readonly chName = 'antenna';
@ -35,7 +35,7 @@ class AntennaChannel extends Channel {
}
@bindThis
private async onEvent(data: StreamMessages['antenna']['payload']) {
private async onEvent(data: GlobalEvents['antenna']['payload']) {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });

View File

@ -1,140 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class CatTimelineChannel extends Channel {
public readonly chName = 'catTimeline';
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: any): Promise<void> {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ctlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
// フォローしているチャンネルの投稿 の場合だけ
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
(note.channelId == null && this.following.has(note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await this.noteEntityService.pack(note.id, this.user!, {
detail: true,
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await this.noteEntityService.pack(note.replyId, this.user!, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, {
detail: true,
});
}
}
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
this.connection.cacheNote(note);
this.send('note', note);
}
@bindThis
public dispose(): void {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}
@Injectable()
export class CatTimelineChannelService {
public readonly shouldShare = CatTimelineChannel.shouldShare;
public readonly requireCredential = CatTimelineChannel.requireCredential;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): CatTimelineChannel {
return new CatTimelineChannel(
this.metaService,
this.roleService,
this.noteEntityService,
id,
connection,
);
}
}

View File

@ -10,8 +10,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
import type { StreamMessages } from '../types.js';
class ChannelChannel extends Channel {
public readonly chName = 'channel';
@ -73,7 +73,7 @@ class ChannelChannel extends Channel {
}
@bindThis
private onEvent(data: StreamMessages['channel']['payload']) {
private onEvent(data: GlobalEvents['channel']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;

View File

@ -19,6 +19,7 @@ class GlobalTimelineChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
private withRenotes: boolean;
constructor(
private metaService: MetaService,
@ -37,7 +38,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return;
this.withReplies = params.withReplies as boolean;
this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class GlobalTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;

View File

@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
private withRenotes: boolean;
constructor(
private noteEntityService: NoteEntityService,
@ -30,7 +31,8 @@ class HomeTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
this.withReplies = params.withReplies as boolean;
this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
this.subscriber.on('notesStream', this.onNote);
}
@ -77,6 +79,8 @@ class HomeTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -19,6 +19,7 @@ class HybridTimelineChannel extends Channel {
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
private withRenotes: boolean;
constructor(
private metaService: MetaService,
@ -37,7 +38,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean;
this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -89,6 +91,8 @@ class HybridTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -18,6 +18,7 @@ class LocalTimelineChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
private withRenotes: boolean;
constructor(
private metaService: MetaService,
@ -36,7 +37,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean;
this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class LocalTimelineChannel extends Channel {
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View File

@ -1,119 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class MediaTimelineChannel extends Channel {
public readonly chName = 'mediaTimeline';
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: any) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.mtlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// リプライなら再pack
if (note.replyId != null) {
note.reply = await this.noteEntityService.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await this.noteEntityService.pack(note.renoteId, this.user, {
detail: true,
});
}
// 関係ない返信は除外
if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
this.connection.cacheNote(note);
this.send('note', note);
}
@bindThis
public dispose() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}
@Injectable()
export class MediaTimelineChannelService {
public readonly shouldShare = MediaTimelineChannel.shouldShare;
public readonly requireCredential = MediaTimelineChannel.requireCredential;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): MediaTimelineChannel {
return new MediaTimelineChannel(
this.metaService,
this.roleService,
this.noteEntityService,
id,
connection,
);
}
}

View File

@ -11,8 +11,8 @@ import { MessagingService } from '@/core/MessagingService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
import type { StreamMessages } from '../types.js';
class MessagingChannel extends Channel {
public readonly chName = 'messaging';
@ -71,7 +71,7 @@ class MessagingChannel extends Channel {
}
@bindThis
private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
private onEvent(data: GlobalEvents['messaging']['payload'] | GlobalEvents['groupMessaging']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;

View File

@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
import { StreamMessages } from '../types.js';
class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline';
@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel {
}
@bindThis
private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
if (data.type === 'note') {
const note = data.body;

View File

@ -1,302 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MiChannel } from '@/models/Channel.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiAntenna } from '@/models/Antenna.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
import type { MiUserList } from '@/models/UserList.js';
import type { MiMessagingMessage } from '@/models/MessagingMessage.js';
import type { MiUserGroup } from '@/models/UserGroup.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiWebhook } from '@/models/Webhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiRole, MiRoleAssignment } from '@/models/_.js';
import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events';
//#region Stream type-body definitions
export interface InternalStreamTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
remoteUserUpdated: { id: MiUser['id']; };
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
policiesUpdated: MiRole['policies'];
roleCreated: MiRole;
roleDeleted: MiRole;
roleUpdated: MiRole;
userRoleAssigned: MiRoleAssignment;
userRoleUnassigned: MiRoleAssignment;
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
metaUpdated: MiMeta;
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
updateUserProfile: MiUserProfile;
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
}
export interface BroadcastTypes {
emojiAdded: {
emoji: Packed<'EmojiDetailed'>;
};
emojiUpdated: {
emojis: Packed<'EmojiDetailed'>[];
};
emojiDeleted: {
emojis: {
id?: string;
name: string;
[other: string]: any;
}[];
};
announcementCreated: {
announcement: Packed<'Announcement'>;
};
}
export interface MainStreamTypes {
notification: Packed<'Notification'>;
mention: Packed<'Note'>;
reply: Packed<'Note'>;
renote: Packed<'Note'>;
follow: Packed<'UserDetailedNotMe'>;
followed: Packed<'User'>;
unfollow: Packed<'User'>;
meUpdated: Packed<'User'>;
pageEvent: {
pageId: MiPage['id'];
event: string;
var: any;
userId: MiUser['id'];
user: Packed<'User'>;
};
urlUploadFinished: {
marker?: string | null;
file: Packed<'DriveFile'>;
};
readAllNotifications: undefined;
unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: MiNote['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllMessagingMessages: undefined;
messagingMessage: Packed<'MessagingMessage'>;
unreadMessagingMessage: Packed<'MessagingMessage'>;
readAllAntennas: undefined;
unreadAntenna: MiAntenna;
readAllAnnouncements: undefined;
myTokenRegenerated: undefined;
signin: MiSignin;
registryUpdated: {
scope?: string[];
key: string;
value: any | null;
};
driveFileCreated: Packed<'DriveFile'>;
readAntenna: MiAntenna;
receiveFollowRequest: Packed<'User'>;
announcementCreated: {
announcement: Packed<'Announcement'>;
};
}
export interface DriveStreamTypes {
fileCreated: Packed<'DriveFile'>;
fileDeleted: MiDriveFile['id'];
fileUpdated: Packed<'DriveFile'>;
folderCreated: Packed<'DriveFolder'>;
folderDeleted: MiDriveFolder['id'];
folderUpdated: Packed<'DriveFolder'>;
}
export interface NoteStreamTypes {
pollVoted: {
choice: number;
userId: MiUser['id'];
};
deleted: {
deletedAt: Date;
};
reacted: {
reaction: string;
emoji?: {
name: string;
url: string;
} | null;
userId: MiUser['id'];
};
unreacted: {
reaction: string;
userId: MiUser['id'];
};
}
type NoteStreamEventTypes = {
[key in keyof NoteStreamTypes]: {
id: MiNote['id'];
body: NoteStreamTypes[key];
};
};
export interface ChannelStreamTypes {
typing: MiUser['id'];
}
export interface UserListStreamTypes {
userAdded: Packed<'User'>;
userRemoved: Packed<'User'>;
}
export interface AntennaStreamTypes {
note: MiNote;
}
export interface MessagingStreamTypes {
read: MiMessagingMessage['id'][];
typing: MiUser['id'];
message: Packed<'MessagingMessage'>;
deleted: MiMessagingMessage['id'];
}
export interface GroupMessagingStreamTypes {
read: {
ids: MiMessagingMessage['id'][];
userId: MiUser['id'];
};
typing: MiUser['id'];
message: Packed<'MessagingMessage'>;
deleted: MiMessagingMessage['id'];
}
export interface MessagingIndexStreamTypes {
read: MiMessagingMessage['id'][];
message: Packed<'MessagingMessage'>;
}
export interface RoleTimelineStreamTypes {
note: Packed<'Note'>;
}
export interface AdminStreamTypes {
newAbuseUserReport: {
id: MiAbuseUserReport['id'];
targetUserId: MiUser['id'],
reporterId: MiUser['id'],
comment: string;
};
}
//#endregion
// 辞書(interface or type)から{ type, body }ユニオンを定義
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
// VS Codeの展開を防止するためにEvents型を定義
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
type EventUnionFromDictionary<
T extends object,
U = Events<T>
> = U[keyof U];
// redis通すとDateのインスタンスはstringに変換されるので
export type Serialized<T> = {
[K in keyof T]:
T[K] extends Date
? string
: T[K] extends (Date | null)
? (string | null)
: T[K] extends Record<string, any>
? Serialized<T[K]>
: T[K];
};
type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
// name/messages(spec) pairs dictionary
export type StreamMessages = {
internal: {
name: 'internal';
payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
};
broadcast: {
name: 'broadcast';
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
};
main: {
name: `mainStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
};
drive: {
name: `driveStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
};
note: {
name: `noteStream:${MiNote['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
};
channel: {
name: `channelStream:${MiChannel['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ChannelStreamTypes>>;
};
userList: {
name: `userListStream:${MiUserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
};
roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>;
};
antenna: {
name: `antennaStream:${MiAntenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
};
messaging: {
name: `messagingStream:${MiUser['id']}-${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingStreamTypes>>;
};
groupMessaging: {
name: `messagingStream:${MiUserGroup['id']}`;
payload: EventUnionFromDictionary<SerializedAll<GroupMessagingStreamTypes>>;
};
messagingIndex: {
name: `messagingIndexStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingIndexStreamTypes>>;
};
admin: {
name: `adminStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
};
notes: {
name: 'notesStream';
payload: Serialized<Packed<'Note'>>;
};
};
// API event definitions
// ストリームごとのEmitterの辞書を用意
type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
// provide stream channels union
export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];

View File

@ -188,7 +188,7 @@ export class ClientServerService {
// Authenticate
fastify.addHook('onRequest', async (request, reply) => {
// %71ueueとかでリクエストされたら困るため
const url = decodeURI(request.url);
const url = decodeURI(request.routerPath);
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
const token = request.cookies.token;
if (token == null) {
@ -732,8 +732,8 @@ export class ClientServerService {
fastify.setErrorHandler(async (error, request, reply) => {
const errId = randomUUID();
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routerPath}: ${error.message}`, {
path: request.routerPath,
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, {
path: request.routeOptions.url,
params: request.params,
query: request.query,
code: error.name,

View File

@ -35,7 +35,7 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.35.0')
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.37.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

View File

@ -57,6 +57,10 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
'createInvitation',
'createAd',
'updateAd',
'deleteAd',
] as const;
export type ModerationLogPayloads = {
@ -199,4 +203,31 @@ export type ModerationLogPayloads = {
report: any;
forwarded: boolean;
};
createInvitation: {
invitations: any[];
};
createAd: {
adId: string;
ad: any;
};
updateAd: {
adId: string;
before: any;
after: any;
};
deleteAd: {
adId: string;
ad: any;
};
};
export type Serialized<T> = {
[K in keyof T]:
T[K] extends Date
? string
: T[K] extends (Date | null)
? (string | null)
: T[K] extends Record<string, any>
? Serialized<T[K]>
: T[K];
};

View File

@ -167,7 +167,7 @@ describe('ユーザー', () => {
unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords,
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes,
achievements: user.achievements,
loggedInDays: user.loggedInDays,
@ -416,7 +416,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []);
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest', 'groupInvited']);
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
@ -497,8 +497,8 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'groupInvited', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest', 'groupInvited'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => {

View File

@ -215,13 +215,6 @@ export type Channels = {
};
receives: null;
};
mediaTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
hybridTimeline: {
params: null;
events: {
@ -229,13 +222,6 @@ export type Channels = {
};
receives: null;
};
catTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
globalTimeline: {
params: null;
events: {
@ -1587,7 +1573,7 @@ export type Endpoints = {
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
mutedWords?: string[][];
mutingNotificationTypes?: Notification_2['type'][];
notificationRecieveConfig?: any;
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
};
@ -2485,8 +2471,6 @@ type LiteInstanceMetadata = {
feedbackUrl: string;
disableRegistration: boolean;
disableLocalTimeline: boolean;
disableMediaTimeline: boolean;
disableTimeline: boolean;
disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number;
@ -2548,7 +2532,22 @@ type MeDetailed = UserDetailed & {
isDeleted: boolean;
isExplorable: boolean;
mutedWords: string[][];
mutingNotificationTypes: string[];
notificationRecieveConfig: {
[notificationType in typeof notificationTypes_2[number]]?: {
type: 'all';
} | {
type: 'never';
} | {
type: 'following';
} | {
type: 'follower';
} | {
type: 'mutualFollow';
} | {
type: 'list';
userListId: string;
};
};
noCrawle: boolean;
receiveAnnouncementEmail: boolean;
usePasswordLessLogin: boolean;
@ -2677,10 +2676,22 @@ type ModerationLog = {
} | {
type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
} | {
type: 'createInvitation';
info: ModerationLogPayloads['createInvitation'];
} | {
type: 'createAd';
info: ModerationLogPayloads['createAd'];
} | {
type: 'updateAd';
info: ModerationLogPayloads['updateAd'];
} | {
type: 'deleteAd';
info: ModerationLogPayloads['deleteAd'];
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd"];
// @public (undocumented)
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
@ -2689,6 +2700,7 @@ export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
type Note = {
id: ID;
createdAt: DateString;
updatedAt?: DateString | null;
text: string | null;
cw: string | null;
user: User;
@ -3033,7 +3045,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:659:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:588:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:602:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View File

@ -20,12 +20,12 @@
"url": "git+https://github.com/misskey-dev/misskey.js.git"
},
"devDependencies": {
"@microsoft/api-extractor": "7.37.0",
"@microsoft/api-extractor": "7.37.2",
"@swc/jest": "0.2.29",
"@types/jest": "29.5.5",
"@types/node": "20.6.4",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@types/node": "20.7.1",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"eslint": "8.50.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
@ -39,7 +39,7 @@
],
"dependencies": {
"@swc/cli": "0.1.62",
"@swc/core": "1.3.87",
"@swc/core": "1.3.90",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View File

@ -437,7 +437,7 @@ export type Endpoints = {
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
mutedWords?: string[][];
mutingNotificationTypes?: Notification['type'][];
notificationRecieveConfig?: any;
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
}; res: MeDetailed; };

View File

@ -74,6 +74,10 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
'createInvitation',
'createAd',
'updateAd',
'deleteAd',
] as const;
export type ModerationLogPayloads = {
@ -216,4 +220,20 @@ export type ModerationLogPayloads = {
report: any;
forwarded: boolean;
};
createInvitation: {
invitations: any[];
};
createAd: {
adId: string;
ad: any;
};
updateAd: {
adId: string;
before: any;
after: any;
};
deleteAd: {
adId: string;
ad: any;
};
};

View File

@ -1,4 +1,4 @@
import { ModerationLogPayloads } from './consts.js';
import { ModerationLogPayloads, notificationTypes } from './consts.js';
export type ID = string;
export type DateString = string;
@ -104,7 +104,22 @@ export type MeDetailed = UserDetailed & {
isDeleted: boolean;
isExplorable: boolean;
mutedWords: string[][];
mutingNotificationTypes: string[];
notificationRecieveConfig: {
[notificationType in typeof notificationTypes[number]]?: {
type: 'all';
} | {
type: 'never';
} | {
type: 'following';
} | {
type: 'follower';
} | {
type: 'mutualFollow';
} | {
type: 'list';
userListId: string;
};
};
noCrawle: boolean;
receiveAnnouncementEmail: boolean;
usePasswordLessLogin: boolean;
@ -162,6 +177,7 @@ export type GalleryPost = {
export type Note = {
id: ID;
createdAt: DateString;
updatedAt?: DateString | null;
text: string | null;
cw: string | null;
user: User;
@ -316,8 +332,6 @@ export type LiteInstanceMetadata = {
feedbackUrl: string;
disableRegistration: boolean;
disableLocalTimeline: boolean;
disableMediaTimeline: boolean;
disableTimeline: boolean;
disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number;
@ -664,4 +678,16 @@ export type ModerationLog = {
} | {
type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
} | {
type: 'createInvitation';
info: ModerationLogPayloads['createInvitation'];
} | {
type: 'createAd';
info: ModerationLogPayloads['createAd'];
} | {
type: 'updateAd';
info: ModerationLogPayloads['updateAd'];
} | {
type: 'deleteAd';
info: ModerationLogPayloads['deleteAd'];
});

View File

@ -56,13 +56,6 @@ export type Channels = {
};
receives: null;
};
mediaTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
hybridTimeline: {
params: null;
events: {
@ -70,13 +63,6 @@ export type Channels = {
};
receives: null;
};
catTimeline: {
params: null;
events: {
note: (payload: Note) => void;
};
receives: null;
};
globalTimeline: {
params: null;
events: {
@ -147,6 +133,13 @@ export type NoteUpdatedEvent = {
body: {
deletedAt: string;
};
} | {
id: Note['id'];
type: 'updated';
body: {
cw: string | null;
text: string;
};
} | {
id: Note['id'];
type: 'pollVoted';

View File

@ -1,6 +1,6 @@
<link rel="preload" href="https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.32.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.37.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style>
html {

View File

@ -24,7 +24,7 @@
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.4",
"@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.35.0",
"@tabler/icons-webfont": "2.37.0",
"@vitejs/plugin-vue": "4.3.4",
"@vue-macros/reactivity-transform": "0.3.23",
"@vue/compiler-sfc": "3.3.4",
@ -33,7 +33,7 @@
"broadcast-channel": "5.3.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1",
"canvas-confetti": "1.6.0",
"canvas-confetti": "1.6.1",
"chart.js": "4.4.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
@ -46,7 +46,7 @@
"cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "^3.0.3",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
"gsap": "3.12.2",
"idb-keyval": "6.2.1",
@ -60,13 +60,13 @@
"prismjs": "1.29.0",
"punycode": "2.3.0",
"querystring": "0.2.1",
"rollup": "3.29.2",
"rollup": "3.29.4",
"sanitize-html": "2.11.0",
"sass": "1.68.0",
"strict-event-emitter-types": "2.0.0",
"temml": "0.10.13",
"textarea-caret": "3.1.0",
"three": "0.156.1",
"three": "0.157.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.8",
@ -74,7 +74,7 @@
"twemoji-parser": "14.0.0",
"typescript": "5.2.2",
"uuid": "9.0.1",
"v-code-diff": "^1.7.1",
"v-code-diff": "1.7.1",
"vanilla-tilt": "1.8.1",
"vite": "4.4.9",
"vue": "3.3.4",
@ -82,46 +82,46 @@
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.4.4",
"@storybook/addon-essentials": "7.4.4",
"@storybook/addon-interactions": "7.4.4",
"@storybook/addon-links": "7.4.4",
"@storybook/addon-storysource": "7.4.4",
"@storybook/addons": "7.4.4",
"@storybook/blocks": "7.4.4",
"@storybook/core-events": "7.4.4",
"@storybook/addon-actions": "7.4.5",
"@storybook/addon-essentials": "7.4.5",
"@storybook/addon-interactions": "7.4.5",
"@storybook/addon-links": "7.4.5",
"@storybook/addon-storysource": "7.4.5",
"@storybook/addons": "7.4.5",
"@storybook/blocks": "7.4.5",
"@storybook/core-events": "7.4.5",
"@storybook/jest": "0.2.2",
"@storybook/manager-api": "7.4.4",
"@storybook/preview-api": "7.4.4",
"@storybook/react": "7.4.4",
"@storybook/react-vite": "7.4.4",
"@storybook/manager-api": "7.4.5",
"@storybook/preview-api": "7.4.5",
"@storybook/react": "7.4.5",
"@storybook/react-vite": "7.4.5",
"@storybook/testing-library": "0.2.1",
"@storybook/theming": "7.4.4",
"@storybook/types": "7.4.4",
"@storybook/vue3": "7.4.4",
"@storybook/vue3-vite": "7.4.4",
"@storybook/theming": "7.4.5",
"@storybook/types": "7.4.5",
"@storybook/vue3": "7.4.5",
"@storybook/vue3-vite": "7.4.5",
"@testing-library/vue": "7.0.0",
"@types/autosize": "^4.0.1",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.2",
"@types/matter-js": "0.19.0",
"@types/micromatch": "4.0.2",
"@types/node": "20.6.4",
"@types/matter-js": "0.19.1",
"@types/micromatch": "4.0.3",
"@types/node": "20.7.1",
"@types/prismjs": "^1.26.0",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0",
"@types/sanitize-html": "2.9.1",
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.4",
"@types/uuid": "9.0.4",
"@types/websocket": "1.0.6",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@types/websocket": "1.0.7",
"@types/ws": "8.5.6",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@vitest/coverage-v8": "0.34.5",
"@vue/runtime-core": "3.3.4",
"acorn": "8.10.0",
"cross-env": "7.0.3",
"cypress": "13.2.0",
"cypress": "13.3.0",
"eslint": "8.50.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-storybook": "^0.6.13",
@ -136,13 +136,13 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.1",
"storybook": "7.4.4",
"storybook": "7.4.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.5",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.13"
"vue-tsc": "1.8.15"
}
}

View File

@ -124,8 +124,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<footer>
<div :class="$style.noteFooterInfo">
<MkA :class="$style.time" :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="detail"/>
<div v-if="appearNote.updatedAt">
{{ i18n.ts.edited }}: <MkTime :class="$style.time" :time="appearNote.updatedAt" mode="detail"/>
</div>
<MkA :to="notePage(appearNote)">
<MkTime :class="$style.time" :time="appearNote.createdAt" mode="detail"/>
</MkA>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>

View File

@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.section">
<div :class="$style.info">
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
<span v-if="note.visibility !== 'public'" style="margin-right: 0.5em;">
<i v-if="note.visibility === 'home'" v-tooltip="i18n.ts._visibility[note.visibility]" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" v-tooltip="i18n.ts._visibility[note.visibility]" class="ti ti-lock"></i>

View File

@ -0,0 +1,78 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
:withOkButton="true"
:okButtonDisabled="false"
@ok="ok()"
@close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.notificationSetting }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
<div class="_buttons">
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref, Ref } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { notificationTypes } from '@/const.js';
import { i18n } from '@/i18n.js';
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
const emit = defineEmits<{
(ev: 'done', v: { excludeTypes: string[] }): void,
(ev: 'closed'): void,
}>();
const props = withDefaults(defineProps<{
excludeTypes?: typeof notificationTypes[number][];
}>(), {
excludeTypes: () => [],
});
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
function ok() {
emit('done', {
excludeTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
.filter(type => !typesMap[type].value),
});
if (dialog) dialog.close();
}
function disableAll() {
for (const type of notificationTypes) {
typesMap[type].value = false;
}
}
function enableAll() {
for (const type of notificationTypes) {
typesMap[type].value = true;
}
}
</script>

View File

@ -1,95 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
:withOkButton="true"
:okButtonDisabled="false"
@ok="ok()"
@close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.notificationSetting }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<template v-if="showGlobalToggle">
<MkSwitch v-model="useGlobalSetting">
{{ i18n.ts.useGlobalSetting }}
<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
</MkSwitch>
</template>
<template v-if="!useGlobalSetting">
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
<div class="_buttons">
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
</template>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref, Ref } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { notificationTypes } from '@/const';
import { i18n } from '@/i18n.js';
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
const emit = defineEmits<{
(ev: 'done', v: { includingTypes: string[] | null }): void,
(ev: 'closed'): void,
}>();
const props = withDefaults(defineProps<{
includingTypes?: typeof notificationTypes[number][] | null;
showGlobalToggle?: boolean;
}>(), {
includingTypes: () => [],
showGlobalToggle: true,
});
let includingTypes = $computed(() => props.includingTypes?.filter(x => notificationTypes.includes(x)) ?? []);
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(includingTypes.includes(t)) }), {} as any);
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
function ok() {
if (useGlobalSetting) {
emit('done', { includingTypes: null });
} else {
emit('done', {
includingTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
.filter(type => typesMap[type].value),
});
}
if (dialog) dialog.close();
}
function disableAll() {
for (const type of notificationTypes) {
typesMap[type].value = false;
}
}
function enableAll() {
for (const type of notificationTypes) {
typesMap[type].value = true;
}
}
</script>

View File

@ -30,13 +30,13 @@ import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { mainRouter } from '@/router.js';
const props = defineProps<{
includeTypes?: typeof notificationTypes[number][];
excludeTypes?: typeof notificationTypes[number][];
}>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
@ -45,13 +45,12 @@ const pagination: Paging = {
endpoint: 'i/notifications' as const,
limit: 10,
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
excludeTypes: props.excludeTypes ?? undefined,
})),
};
const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
if (isMuted || document.visibilityState === 'visible') {
useStream().send('readNotification');
}

View File

@ -149,6 +149,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: true,
@ -744,19 +745,20 @@ async function post(ev?: MouseEvent) {
}
let postData = {
text: text === '' ? undefined : text,
text: text === '' ? null : text,
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
channelId: props.channel ? props.channel.id : undefined,
poll: poll,
event: event,
cw: useCw ? cw ?? '' : undefined,
cw: useCw ? cw ?? '' : null,
localOnly: localOnly,
visibility: visibility,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
reactionAcceptance,
disableRightClick: disableRightClick,
noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
@ -779,7 +781,7 @@ async function post(ev?: MouseEvent) {
}
posting = true;
os.api('notes/create', postData, token).then(() => {
os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted = true;
} else {
@ -872,8 +874,10 @@ function showActions(ev) {
action: () => {
action.handler({
text: text,
cw: cw,
}, (key, value) => {
if (key === 'text') { text = value; }
if (key === 'cw') { useCw = value !== null; cw = value; }
});
},
})), ev.currentTarget ?? ev.target);

View File

@ -30,6 +30,7 @@ const props = defineProps<{
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
updateMode?: boolean;
}>();
const emit = defineEmits<{

View File

@ -169,6 +169,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: false,
@ -762,19 +763,20 @@ async function post(ev?: MouseEvent) {
}
let postData = {
text: text === '' ? undefined : text,
text: text === '' ? null : text,
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
channelId: props.channel ? props.channel.id : undefined,
poll: poll,
event: event,
cw: useCw ? cw ?? '' : undefined,
cw: useCw ? cw ?? '' : null,
localOnly: localOnly,
visibility: visibility,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
reactionAcceptance,
disableRightClick: disableRightClick,
noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
@ -797,7 +799,7 @@ async function post(ev?: MouseEvent) {
}
posting = true;
os.api('notes/create', postData, token).then(() => {
os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted = true;
} else {
@ -890,8 +892,10 @@ function showActions(ev) {
action: () => {
action.handler({
text: text,
cw: cw,
}, (key, value) => {
if (key === 'text') { text = value; }
if (key === 'cw') { useCw = value !== null; cw = value; }
});
},
})), ev.currentTarget ?? ev.target);

View File

@ -20,14 +20,23 @@ import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
src: string;
list?: string;
antenna?: string;
channel?: string;
role?: string;
sound?: boolean;
}>();
withRenotes?: boolean;
withReplies?: boolean;
onlyFiles?: boolean;
onlyCats?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
onlyFiles: false,
onlyCats: false,
});
const emit = defineEmits<{
(ev: 'note'): void;
@ -83,10 +92,16 @@ if (props.src === 'antenna') {
} else if (props.src === 'home') {
endpoint = 'notes/timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('homeTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
@ -98,62 +113,59 @@ if (props.src === 'antenna') {
} else if (props.src === 'local') {
endpoint = 'notes/local-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('localTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
tlIcon = 'ti ti-planet';
tlHint = i18n.ts._tlTutorial.step1_2;
tlHintClosed = defaultStore.state.tlLocalHintClosed;
} else if (props.src === 'media') {
endpoint = 'notes/media-timeline';
connection = stream.useChannel('mediaTimeline');
connection.on('note', prependFilterdMedia);
tlIcon = 'ti ti-photo';
tlHint = i18n.ts._tlTutorial.step1_3;
tlHintClosed = defaultStore.state.tlMediaHintClosed;
} else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('hybridTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
tlIcon = 'ti ti-rocket';
tlHint = i18n.ts._tlTutorial.step1_4;
tlIcon = 'ti ti-universe';
tlHint = i18n.ts._tlTutorial.step1_3;
tlHintClosed = defaultStore.state.tlSocialHintClosed;
} else if (props.src === 'cat') {
endpoint = 'notes/cat-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('catTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
tlIcon = 'ti ti-cat';
tlHint = i18n.ts._tlTutorial.step1_5;
tlHintClosed = defaultStore.state.tlCatHintClosed;
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
};
connection = stream.useChannel('globalTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
});
connection.on('note', prepend);
tlIcon = 'ti ti-world';
tlHint = i18n.ts._tlTutorial.step1_6;
tlHint = i18n.ts._tlTutorial.step1_4;
tlHintClosed = defaultStore.state.tlGlobalHintClosed;
} else if (props.src === 'mentions') {
endpoint = 'notes/mentions';
@ -174,9 +186,17 @@ if (props.src === 'antenna') {
} else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
listId: props.list,
};
connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
withCats: props.onlyCats,
listId: props.list,
});
connection.on('note', prepend);
@ -208,15 +228,9 @@ function closeHint() {
case 'local':
defaultStore.set('tlLocalHintClosed', true);
break;
case 'media':
defaultStore.set('tlMediaHintClosed', true);
break;
case 'social':
defaultStore.set('tlSocialHintClosed', true);
break;
case 'cat':
defaultStore.set('tlCatHintClosed', true);
break;
case 'global':
defaultStore.set('tlGlobalHintClosed', true);
break;

Some files were not shown because too many files have changed in this diff Show More