commit
f318e165a0
154 changed files with 3513 additions and 1644 deletions
|
@ -206,3 +206,6 @@ signToActivityPubGet: true
|
||||||
|
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# PID File of master process
|
||||||
|
#pidFile: /tmp/misskey.pid
|
||||||
|
|
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -12,18 +12,46 @@
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## next
|
## 2023.10.0
|
||||||
|
|
||||||
### General
|
|
||||||
- Enhance: タイムラインからRenoteを除外するオプションを追加
|
|
||||||
- Enhance: ユーザーページのノート一覧でRenoteを除外できるように
|
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
|
||||||
|
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
|
||||||
|
|
||||||
|
## 2023.9.3
|
||||||
|
### General
|
||||||
|
- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: AiScriptでホストのアドレスを参照する定数`SERVER_URL`を追加
|
||||||
- Enhance: モデレーションログ機能の強化
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Fix: Redisに古いバージョンのキャッシュが残っている場合、キャッシュが消えるまでの間通知が届かなくなる問題を修正
|
||||||
|
- Fix: 後方互換性の修正
|
||||||
|
|
||||||
|
## 2023.9.2
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: ノートの編集をできるように
|
||||||
|
- ロールで編集可否を設定可能
|
||||||
|
- Feat: 通知を種類ごとに 全員から受け取る/フォロー中のユーザーのみ受け取る/フォロワーのみ受け取る/相互のみ受け取る/指定したリストのメンバーのみ受け取る/受け取らない から選べるように
|
||||||
|
- Enhance: タイムラインからRenoteを除外するオプションを追加
|
||||||
|
- Enhance: ユーザーページのノート一覧でRenoteを除外できるように
|
||||||
|
- Enhance: タイムラインでファイルが添付されたノートのみ表示するオプションを追加
|
||||||
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
||||||
|
### Client
|
||||||
- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように
|
- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように
|
||||||
|
- Enhance: admin/ad/listにて掲載中の広告が絞り込めるように
|
||||||
|
- Enhance: AiScriptにリモートサーバーのAPIを叩く用の関数を追加(`Mk:apiExternal`)
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: MasterプロセスのPIDを書き出せるように
|
- Enhance: MasterプロセスのPIDを書き出せるように
|
||||||
|
- Enhance: admin/ad/createにてレスポンス200、設定した広告情報を返すように
|
||||||
|
|
||||||
## 2023.9.1
|
## 2023.9.1
|
||||||
|
|
||||||
|
|
|
@ -2039,3 +2039,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "Zmrazit"
|
suspend: "Zmrazit"
|
||||||
resetPassword: "Resetovat heslo"
|
resetPassword: "Resetovat heslo"
|
||||||
|
createInvitation: "Vygenerovat pozvánku"
|
||||||
|
|
|
@ -1120,6 +1120,12 @@ notifyNotes: "Über neue Notizen benachrichtigen"
|
||||||
unnotifyNotes: "Nicht über neue Notizen benachrichtigen"
|
unnotifyNotes: "Nicht über neue Notizen benachrichtigen"
|
||||||
authentication: "Authentifikation"
|
authentication: "Authentifikation"
|
||||||
authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren"
|
authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren"
|
||||||
|
dateAndTime: "Zeit"
|
||||||
|
showRenotes: "Renotes anzeigen"
|
||||||
|
edited: "Bearbeitet"
|
||||||
|
notificationRecieveConfig: "Benachrichtigungseinstellungen"
|
||||||
|
mutualFollow: "Gegenseitig gefolgt"
|
||||||
|
fileAttachedOnly: "Nur Notizen mit Dateien"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Nur für existierende Nutzer"
|
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."
|
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."
|
||||||
|
@ -1450,6 +1456,7 @@ _role:
|
||||||
gtlAvailable: "Kann auf die globale Chronik zugreifen"
|
gtlAvailable: "Kann auf die globale Chronik zugreifen"
|
||||||
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
|
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
|
||||||
canPublicNote: "Kann öffentliche Notizen erstellen"
|
canPublicNote: "Kann öffentliche Notizen erstellen"
|
||||||
|
canEditNote: "Notizbearbeitung"
|
||||||
canInvite: "Erstellung von Einladungscodes für diese Instanz"
|
canInvite: "Erstellung von Einladungscodes für diese Instanz"
|
||||||
inviteLimit: "Maximalanzahl an Einladungen"
|
inviteLimit: "Maximalanzahl an Einladungen"
|
||||||
inviteLimitCycle: "Zyklus des Einladungslimits"
|
inviteLimitCycle: "Zyklus des Einladungslimits"
|
||||||
|
@ -2101,6 +2108,8 @@ _webhookSettings:
|
||||||
reaction: "Wenn du eine Reaktion erhältst"
|
reaction: "Wenn du eine Reaktion erhältst"
|
||||||
mention: "Wenn du erwähnt wirst"
|
mention: "Wenn du erwähnt wirst"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "Rolle erstellt"
|
||||||
|
deleteRole: "Rolle gelöscht"
|
||||||
updateRole: "Rolle aktualisiert"
|
updateRole: "Rolle aktualisiert"
|
||||||
assignRole: "Zu Rolle zugewiesen"
|
assignRole: "Zu Rolle zugewiesen"
|
||||||
unassignRole: "Aus Rolle entfernt"
|
unassignRole: "Aus Rolle entfernt"
|
||||||
|
@ -2124,3 +2133,8 @@ _moderationLogTypes:
|
||||||
unsuspendRemoteInstance: "Fremde Instanz entsperrt"
|
unsuspendRemoteInstance: "Fremde Instanz entsperrt"
|
||||||
markSensitiveDriveFile: "Datei als sensitiv markiert"
|
markSensitiveDriveFile: "Datei als sensitiv markiert"
|
||||||
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"
|
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"
|
||||||
|
resolveAbuseReport: "Meldung bearbeitet"
|
||||||
|
createInvitation: "Einladung erstellt"
|
||||||
|
createAd: "Werbung erstellt"
|
||||||
|
deleteAd: "Werbung gelöscht"
|
||||||
|
updateAd: "Werbung aktualisiert"
|
||||||
|
|
|
@ -164,6 +164,8 @@ flagAsBot: "Mark this account as a bot"
|
||||||
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
|
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
|
||||||
flagAsCat: "Mark this account as a cat"
|
flagAsCat: "Mark this account as a cat"
|
||||||
flagAsCatDescription: "Enable this option to mark this account as a cat."
|
flagAsCatDescription: "Enable this option to mark this account as a cat."
|
||||||
|
flagSpeakAsCat: "Speak as a cat"
|
||||||
|
flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode."
|
||||||
flagShowTimelineReplies: "Show replies in timeline"
|
flagShowTimelineReplies: "Show replies in timeline"
|
||||||
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
|
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
|
||||||
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
|
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
|
||||||
|
@ -1120,7 +1122,12 @@ notifyNotes: "Notify about new notes"
|
||||||
unnotifyNotes: "Stop notifying about new notes"
|
unnotifyNotes: "Stop notifying about new notes"
|
||||||
authentication: "Authentication"
|
authentication: "Authentication"
|
||||||
authenticationRequiredToContinue: "Please authenticate to continue"
|
authenticationRequiredToContinue: "Please authenticate to continue"
|
||||||
showRenotes: "Include renotes"
|
dateAndTime: "Timestamp"
|
||||||
|
showRenotes: "Show renotes"
|
||||||
|
edited: "Edited"
|
||||||
|
notificationRecieveConfig: "Notification Settings"
|
||||||
|
mutualFollow: "Mutual follow"
|
||||||
|
fileAttachedOnly: "Only notes with files"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Existing users only"
|
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."
|
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."
|
||||||
|
@ -1451,6 +1458,7 @@ _role:
|
||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
canPublicNote: "Can send public notes"
|
||||||
|
canEditNote: "Note editing"
|
||||||
canInvite: "Can create instance invite codes"
|
canInvite: "Can create instance invite codes"
|
||||||
inviteLimit: "Invite limit"
|
inviteLimit: "Invite limit"
|
||||||
inviteLimitCycle: "Invite limit cooldown"
|
inviteLimitCycle: "Invite limit cooldown"
|
||||||
|
@ -2102,6 +2110,8 @@ _webhookSettings:
|
||||||
reaction: "When receiving a reaction"
|
reaction: "When receiving a reaction"
|
||||||
mention: "When being mentioned"
|
mention: "When being mentioned"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "Role created"
|
||||||
|
deleteRole: "Role deleted"
|
||||||
updateRole: "Role updated"
|
updateRole: "Role updated"
|
||||||
assignRole: "Assigned to role"
|
assignRole: "Assigned to role"
|
||||||
unassignRole: "Removed from role"
|
unassignRole: "Removed from role"
|
||||||
|
@ -2125,3 +2135,81 @@ _moderationLogTypes:
|
||||||
unsuspendRemoteInstance: "Remote instance unsuspended"
|
unsuspendRemoteInstance: "Remote instance unsuspended"
|
||||||
markSensitiveDriveFile: "File marked as sensitive"
|
markSensitiveDriveFile: "File marked as sensitive"
|
||||||
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
||||||
|
resolveAbuseReport: "Report resolved"
|
||||||
|
createInvitation: "Invite generated"
|
||||||
|
createAd: "Ad created"
|
||||||
|
deleteAd: "Ad deleted"
|
||||||
|
updateAd: "Ad updated"
|
||||||
|
_mfm:
|
||||||
|
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
||||||
|
dummy: "Sharkey expands the world of the Fediverse"
|
||||||
|
mention: "Mention"
|
||||||
|
mentionDescription: "You can specify a user by using an At-Symbol and a username."
|
||||||
|
hashtag: "Hashtag"
|
||||||
|
hashtagDescription: "You can specify a hashtag using a number sign and text."
|
||||||
|
url: "URL"
|
||||||
|
urlDescription: "URLs can be displayed."
|
||||||
|
link: "Link"
|
||||||
|
linkDescription: "Specific parts of text can be displayed as a URL."
|
||||||
|
bold: "Bold"
|
||||||
|
boldDescription: "Highlights letters by making them thicker."
|
||||||
|
small: "Small"
|
||||||
|
smallDescription: "Displays content small and thin."
|
||||||
|
center: "Center"
|
||||||
|
centerDescription: "Displays content centered."
|
||||||
|
inlineCode: "Code (Inline)"
|
||||||
|
inlineCodeDescription: "Displays inline syntax highlighting for (program) code."
|
||||||
|
blockCode: "Code (Block)"
|
||||||
|
blockCodeDescription: "Displays syntax highlighting for multi-line (program) code in a block."
|
||||||
|
inlineMath: "Math (Inline)"
|
||||||
|
inlineMathDescription: "Display math formulas (KaTeX) in-line"
|
||||||
|
blockMath: "Math (Block)"
|
||||||
|
blockMathDescription: "Display math formulas (KaTeX) in a block"
|
||||||
|
quote: "Quote"
|
||||||
|
quoteDescription: "Displays content as a quote."
|
||||||
|
emoji: "Custom Emoji"
|
||||||
|
emojiDescription: "By surrounding a custom emoji name with colons, custom emoji can be displayed."
|
||||||
|
search: "Search"
|
||||||
|
searchDescription: "Displays a search box with pre-entered text."
|
||||||
|
flip: "Flip"
|
||||||
|
flipDescription: "Flips content horizontally or vertically."
|
||||||
|
jelly: "Animation (Jelly)"
|
||||||
|
jellyDescription: "Gives content a jelly-like animation."
|
||||||
|
tada: "Animation (Tada)"
|
||||||
|
tadaDescription: "Gives content a \"Tada!\"-like animation."
|
||||||
|
jump: "Animation (Jump)"
|
||||||
|
jumpDescription: "Gives content a jumping animation."
|
||||||
|
bounce: "Animation (Bounce)"
|
||||||
|
bounceDescription: "Gives content a bouncy animation."
|
||||||
|
shake: "Animation (Shake)"
|
||||||
|
shakeDescription: "Gives content a shaking animation."
|
||||||
|
twitch: "Animation (Twitch)"
|
||||||
|
twitchDescription: "Gives content a strongly twitching animation."
|
||||||
|
spin: "Animation (Spin)"
|
||||||
|
spinDescription: "Gives content a spinning animation."
|
||||||
|
x2: "Big"
|
||||||
|
x2Description: "Displays content bigger."
|
||||||
|
x3: "Very big"
|
||||||
|
x3Description: "Displays content even bigger."
|
||||||
|
x4: "Unbelievably big"
|
||||||
|
x4Description: "Displays content even bigger than bigger than big."
|
||||||
|
blur: "Blur"
|
||||||
|
blurDescription: "Blurs content. It will be displayed clearly when hovered over."
|
||||||
|
font: "Font"
|
||||||
|
fontDescription: "Sets the font to display content in."
|
||||||
|
rainbow: "Rainbow"
|
||||||
|
rainbowDescription: "Makes the content appear in rainbow colors."
|
||||||
|
sparkle: "Sparkle"
|
||||||
|
sparkleDescription: "Gives content a sparkling particle effect."
|
||||||
|
rotate: "Rotate"
|
||||||
|
rotateDescription: "Turns content by a specified angle."
|
||||||
|
position: "Position"
|
||||||
|
positionDescription: "Move content by a specified amount."
|
||||||
|
scale: "Scale"
|
||||||
|
scaleDescription: "Scale content by a specified amount."
|
||||||
|
foreground: "Foreground color"
|
||||||
|
foregroundDescription: "Change the foreground color of text."
|
||||||
|
background: "Background color"
|
||||||
|
backgroundDescription: "Change the background color of text."
|
||||||
|
plain: "Plain"
|
||||||
|
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
|
||||||
|
|
|
@ -418,6 +418,7 @@ moderator: "Moderador"
|
||||||
moderation: "Moderación"
|
moderation: "Moderación"
|
||||||
moderationNote: "Nota de moderación"
|
moderationNote: "Nota de moderación"
|
||||||
addModerationNote: "Añadir nota de moderación"
|
addModerationNote: "Añadir nota de moderación"
|
||||||
|
moderationLogs: "Log de moderación"
|
||||||
nUsersMentioned: "{n} usuarios mencionados"
|
nUsersMentioned: "{n} usuarios mencionados"
|
||||||
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
|
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
|
||||||
securityKey: "Clave de seguridad"
|
securityKey: "Clave de seguridad"
|
||||||
|
@ -710,6 +711,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"
|
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
|
||||||
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
||||||
disableShowingAnimatedImages: "No reproducir imágenes animadas"
|
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."
|
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"
|
notSet: "Sin especificar"
|
||||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||||
|
@ -1109,6 +1111,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."
|
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
|
||||||
replies: "Responder"
|
replies: "Responder"
|
||||||
renotes: "Renotar"
|
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:
|
_announcement:
|
||||||
forExistingUsers: "Solo para usuarios registrados"
|
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."
|
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."
|
||||||
|
@ -1137,7 +1149,13 @@ _serverRules:
|
||||||
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
|
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "URL del ícono"
|
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"
|
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:
|
_accountMigration:
|
||||||
moveFrom: "Trasladar de otra cuenta a ésta"
|
moveFrom: "Trasladar de otra cuenta a ésta"
|
||||||
moveFromSub: "Crear un alias para otra cuenta."
|
moveFromSub: "Crear un alias para otra cuenta."
|
||||||
|
@ -1784,6 +1802,7 @@ _antennaSources:
|
||||||
homeTimeline: "Notas de los usuarios que sigues"
|
homeTimeline: "Notas de los usuarios que sigues"
|
||||||
users: "Notas de un usuario o varios"
|
users: "Notas de un usuario o varios"
|
||||||
userList: "Notas de los usuarios de una lista"
|
userList: "Notas de los usuarios de una lista"
|
||||||
|
userBlacklist: "Todas las notas excepto aquellas de uno o más usuarios especificados"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "Domingo"
|
sunday: "Domingo"
|
||||||
monday: "Lunes"
|
monday: "Lunes"
|
||||||
|
@ -1883,6 +1902,7 @@ _profile:
|
||||||
metadataContent: "Contenido"
|
metadataContent: "Contenido"
|
||||||
changeAvatar: "Cambiar avatar"
|
changeAvatar: "Cambiar avatar"
|
||||||
changeBanner: "Cambiar banner"
|
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:
|
_exportOrImport:
|
||||||
allNotes: "Todas las notas"
|
allNotes: "Todas las notas"
|
||||||
favoritedNotes: "Notas favoritas"
|
favoritedNotes: "Notas favoritas"
|
||||||
|
@ -2001,6 +2021,7 @@ _notification:
|
||||||
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
|
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
|
||||||
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
||||||
pollEnded: "Estan disponibles los resultados de la encuesta"
|
pollEnded: "Estan disponibles los resultados de la encuesta"
|
||||||
|
newNote: "Nueva nota"
|
||||||
unreadAntennaNote: "Antena {name}"
|
unreadAntennaNote: "Antena {name}"
|
||||||
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
|
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
|
||||||
achievementEarned: "Logro desbloqueado"
|
achievementEarned: "Logro desbloqueado"
|
||||||
|
@ -2010,6 +2031,7 @@ _notification:
|
||||||
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
|
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
|
||||||
_types:
|
_types:
|
||||||
all: "Todo"
|
all: "Todo"
|
||||||
|
note: "Nuevas notas"
|
||||||
follow: "Siguiendo"
|
follow: "Siguiendo"
|
||||||
mention: "Menciones"
|
mention: "Menciones"
|
||||||
reply: "Respuestas"
|
reply: "Respuestas"
|
||||||
|
@ -2080,5 +2102,30 @@ _webhookSettings:
|
||||||
reaction: "Cuando se recibe una reacción"
|
reaction: "Cuando se recibe una reacción"
|
||||||
mention: "Cuando hay una mención"
|
mention: "Cuando hay una mención"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "Rol creado"
|
||||||
|
deleteRole: "Rol eliminado"
|
||||||
|
updateRole: "Rol actualizado"
|
||||||
|
assignRole: "Rol asignado"
|
||||||
|
unassignRole: "Rol retirado"
|
||||||
suspend: "Suspender"
|
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"
|
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"
|
||||||
|
|
|
@ -1100,6 +1100,7 @@ currentAnnouncements: "Pengumuman Saat Ini"
|
||||||
pastAnnouncements: "Pengumuman Terdahulu"
|
pastAnnouncements: "Pengumuman Terdahulu"
|
||||||
replies: "Balas"
|
replies: "Balas"
|
||||||
renotes: "Renote"
|
renotes: "Renote"
|
||||||
|
dateAndTime: "Tanggal dan Waktu"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Akun kamu telah sukses dibuat!"
|
accountCreated: "Akun kamu telah sukses dibuat!"
|
||||||
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
|
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
|
||||||
|
@ -2044,3 +2045,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "Tangguhkan"
|
suspend: "Tangguhkan"
|
||||||
resetPassword: "Atur ulang kata sandi"
|
resetPassword: "Atur ulang kata sandi"
|
||||||
|
createInvitation: "Buat kode undangan"
|
||||||
|
|
9
locales/index.d.ts
vendored
9
locales/index.d.ts
vendored
|
@ -1125,6 +1125,10 @@ export interface Locale {
|
||||||
"authenticationRequiredToContinue": string;
|
"authenticationRequiredToContinue": string;
|
||||||
"dateAndTime": string;
|
"dateAndTime": string;
|
||||||
"showRenotes": string;
|
"showRenotes": string;
|
||||||
|
"edited": string;
|
||||||
|
"notificationRecieveConfig": string;
|
||||||
|
"mutualFollow": string;
|
||||||
|
"fileAttachedOnly": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -1538,6 +1542,7 @@ export interface Locale {
|
||||||
"gtlAvailable": string;
|
"gtlAvailable": string;
|
||||||
"ltlAvailable": string;
|
"ltlAvailable": string;
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
|
"canEditNote": string;
|
||||||
"canInvite": string;
|
"canInvite": string;
|
||||||
"inviteLimit": string;
|
"inviteLimit": string;
|
||||||
"inviteLimitCycle": string;
|
"inviteLimitCycle": string;
|
||||||
|
@ -1557,6 +1562,7 @@ export interface Locale {
|
||||||
"descriptionOfRateLimitFactor": string;
|
"descriptionOfRateLimitFactor": string;
|
||||||
"canHideAds": string;
|
"canHideAds": string;
|
||||||
"canSearchNotes": string;
|
"canSearchNotes": string;
|
||||||
|
"canUseTranslator": string;
|
||||||
};
|
};
|
||||||
"_condition": {
|
"_condition": {
|
||||||
"isLocal": string;
|
"isLocal": string;
|
||||||
|
@ -2279,6 +2285,9 @@ export interface Locale {
|
||||||
"unmarkSensitiveDriveFile": string;
|
"unmarkSensitiveDriveFile": string;
|
||||||
"resolveAbuseReport": string;
|
"resolveAbuseReport": string;
|
||||||
"createInvitation": string;
|
"createInvitation": string;
|
||||||
|
"createAd": string;
|
||||||
|
"deleteAd": string;
|
||||||
|
"updateAd": string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
|
|
|
@ -130,8 +130,8 @@ unmarkAsSensitive: "Non segnare come esplicito "
|
||||||
enterFileName: "Nome del file"
|
enterFileName: "Nome del file"
|
||||||
mute: "Silenzia"
|
mute: "Silenzia"
|
||||||
unmute: "Riattiva l'audio"
|
unmute: "Riattiva l'audio"
|
||||||
renoteMute: "Silenzia i Rinota"
|
renoteMute: "Silenzia le Rinota"
|
||||||
renoteUnmute: "Non silenziare i Rinota"
|
renoteUnmute: "Non silenziare le Rinota"
|
||||||
block: "Blocca"
|
block: "Blocca"
|
||||||
unblock: "Sblocca"
|
unblock: "Sblocca"
|
||||||
suspend: "Sospensione"
|
suspend: "Sospensione"
|
||||||
|
@ -991,7 +991,7 @@ thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva"
|
||||||
thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
|
thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
|
||||||
thisPostMayBeAnnoyingCancel: "Annulla"
|
thisPostMayBeAnnoyingCancel: "Annulla"
|
||||||
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
|
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
|
||||||
collapseRenotes: "Comprimi i Rinota già letti"
|
collapseRenotes: "Comprimi le Rinota già viste"
|
||||||
internalServerError: "Errore interno del server"
|
internalServerError: "Errore interno del server"
|
||||||
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
|
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
|
||||||
copyErrorInfo: "Copia le informazioni sull'errore"
|
copyErrorInfo: "Copia le informazioni sull'errore"
|
||||||
|
@ -1120,6 +1120,8 @@ notifyNotes: "Notifica nuove Note"
|
||||||
unnotifyNotes: "Interrompi le notifiche di nuove Note"
|
unnotifyNotes: "Interrompi le notifiche di nuove Note"
|
||||||
authentication: "Autenticazione"
|
authentication: "Autenticazione"
|
||||||
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
|
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
|
||||||
|
dateAndTime: "Data e Ora"
|
||||||
|
showRenotes: "Leggi le Rinota"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo ai profili attuali"
|
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."
|
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."
|
||||||
|
@ -2101,6 +2103,8 @@ _webhookSettings:
|
||||||
reaction: "Quando ricevo una reazione"
|
reaction: "Quando ricevo una reazione"
|
||||||
mention: "Quando mi menzionano"
|
mention: "Quando mi menzionano"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "Ruolo creato"
|
||||||
|
deleteRole: "Ruolo eliminato"
|
||||||
updateRole: "Ruolo aggiornato"
|
updateRole: "Ruolo aggiornato"
|
||||||
assignRole: "Ruolo assegnato"
|
assignRole: "Ruolo assegnato"
|
||||||
unassignRole: "Ruolo disassegnato"
|
unassignRole: "Ruolo disassegnato"
|
||||||
|
@ -2124,3 +2128,5 @@ _moderationLogTypes:
|
||||||
unsuspendRemoteInstance: "Istanza remota riattivata"
|
unsuspendRemoteInstance: "Istanza remota riattivata"
|
||||||
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
|
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
|
||||||
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
|
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
|
||||||
|
resolveAbuseReport: "Segnalazione risolta"
|
||||||
|
createInvitation: "Genera codice di invito"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
_lang_: "日本語"
|
_lang_: "日本語"
|
||||||
|
|
||||||
headlineMisskey: "ノートでつながるネットワーク"
|
headlineMisskey: "ノートでつながるネットワーク"
|
||||||
introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
|
introMisskey: "ようこそ!Sharkeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
|
||||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつです。"
|
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Sharkey</b>のサーバーのひとつです。"
|
||||||
monthAndDay: "{month}月 {day}日"
|
monthAndDay: "{month}月 {day}日"
|
||||||
search: "検索"
|
search: "検索"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
|
@ -161,7 +161,7 @@ youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全ての
|
||||||
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
||||||
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
|
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
|
||||||
flagAsBot: "Botとして設定"
|
flagAsBot: "Botとして設定"
|
||||||
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Sharkeyのシステム上での扱いがBotに合ったものになります。"
|
||||||
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
||||||
flagAsCatDescription: "にゃにゃにゃ??"
|
flagAsCatDescription: "にゃにゃにゃ??"
|
||||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
|
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
|
||||||
|
@ -221,7 +221,7 @@ noUsers: "ユーザーはいません"
|
||||||
editProfile: "プロフィールを編集"
|
editProfile: "プロフィールを編集"
|
||||||
noteDeleteConfirm: "このノートを削除しますか?"
|
noteDeleteConfirm: "このノートを削除しますか?"
|
||||||
pinLimitExceeded: "これ以上ピン留めできません"
|
pinLimitExceeded: "これ以上ピン留めできません"
|
||||||
intro: "Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。"
|
intro: "Sharkeyのインストールが完了しました!管理者アカウントを作成しましょう。"
|
||||||
done: "完了"
|
done: "完了"
|
||||||
processing: "処理中"
|
processing: "処理中"
|
||||||
preview: "プレビュー"
|
preview: "プレビュー"
|
||||||
|
@ -407,7 +407,7 @@ exploreFediverse: "Fediverseを探索"
|
||||||
popularTags: "人気のタグ"
|
popularTags: "人気のタグ"
|
||||||
userList: "リスト"
|
userList: "リスト"
|
||||||
about: "情報"
|
about: "情報"
|
||||||
aboutMisskey: "Misskeyについて"
|
aboutMisskey: "Sharkeyについて"
|
||||||
administrator: "管理者"
|
administrator: "管理者"
|
||||||
token: "確認コード"
|
token: "確認コード"
|
||||||
2fa: "二要素認証"
|
2fa: "二要素認証"
|
||||||
|
@ -555,7 +555,7 @@ sort: "ソート"
|
||||||
ascendingOrder: "昇順"
|
ascendingOrder: "昇順"
|
||||||
descendingOrder: "降順"
|
descendingOrder: "降順"
|
||||||
scratchpad: "スクラッチパッド"
|
scratchpad: "スクラッチパッド"
|
||||||
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
|
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Sharkeyと対話するコードの記述、実行、結果の確認ができます。"
|
||||||
output: "出力"
|
output: "出力"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||||
|
@ -687,7 +687,7 @@ unclip: "クリップ解除"
|
||||||
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
|
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
|
||||||
public: "パブリック"
|
public: "パブリック"
|
||||||
private: "非公開"
|
private: "非公開"
|
||||||
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
i18nInfo: "Sharkeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||||
manageAccessTokens: "アクセストークンの管理"
|
manageAccessTokens: "アクセストークンの管理"
|
||||||
accountInfo: "アカウント情報"
|
accountInfo: "アカウント情報"
|
||||||
notesCount: "ノートの数"
|
notesCount: "ノートの数"
|
||||||
|
@ -741,7 +741,7 @@ onlineUsersCount: "{n}人がオンライン"
|
||||||
nUsers: "{n}ユーザー"
|
nUsers: "{n}ユーザー"
|
||||||
nNotes: "{n}ノート"
|
nNotes: "{n}ノート"
|
||||||
sendErrorReports: "エラーリポートを送信"
|
sendErrorReports: "エラーリポートを送信"
|
||||||
sendErrorReportsDescription: "オンにすると、問題が発生したときにエラーの詳細情報がMisskeyに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。"
|
sendErrorReportsDescription: "オンにすると、問題が発生したときにエラーの詳細情報がSharkeyに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。"
|
||||||
myTheme: "マイテーマ"
|
myTheme: "マイテーマ"
|
||||||
backgroundColor: "背景"
|
backgroundColor: "背景"
|
||||||
accentColor: "アクセント"
|
accentColor: "アクセント"
|
||||||
|
@ -835,7 +835,7 @@ hashtags: "ハッシュタグ"
|
||||||
troubleshooting: "トラブルシューティング"
|
troubleshooting: "トラブルシューティング"
|
||||||
useBlurEffect: "UIにぼかし効果を使用"
|
useBlurEffect: "UIにぼかし効果を使用"
|
||||||
learnMore: "詳しく"
|
learnMore: "詳しく"
|
||||||
misskeyUpdated: "Misskeyが更新されました!"
|
misskeyUpdated: "Sharkeyが更新されました!"
|
||||||
whatIsNew: "更新情報を見る"
|
whatIsNew: "更新情報を見る"
|
||||||
translate: "翻訳"
|
translate: "翻訳"
|
||||||
translatedFrom: "{x}から翻訳"
|
translatedFrom: "{x}から翻訳"
|
||||||
|
@ -964,8 +964,8 @@ numberOfLikes: "いいね数"
|
||||||
show: "表示"
|
show: "表示"
|
||||||
neverShow: "今後表示しない"
|
neverShow: "今後表示しない"
|
||||||
remindMeLater: "また後で"
|
remindMeLater: "また後で"
|
||||||
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
didYouLikeMisskey: "Sharkeyを気に入っていただけましたか?"
|
||||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
pleaseDonate: "Sharkeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||||
roles: "ロール"
|
roles: "ロール"
|
||||||
role: "ロール"
|
role: "ロール"
|
||||||
noRole: "ロールはありません"
|
noRole: "ロールはありません"
|
||||||
|
@ -1075,7 +1075,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロー
|
||||||
cancelReactionConfirm: "リアクションを取り消しますか?"
|
cancelReactionConfirm: "リアクションを取り消しますか?"
|
||||||
changeReactionConfirm: "リアクションを変更しますか?"
|
changeReactionConfirm: "リアクションを変更しますか?"
|
||||||
later: "あとで"
|
later: "あとで"
|
||||||
goToMisskey: "Misskeyへ"
|
goToMisskey: "Sharkeyへ"
|
||||||
additionalEmojiDictionary: "絵文字の追加辞書"
|
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
installed: "インストール済み"
|
installed: "インストール済み"
|
||||||
branding: "ブランディング"
|
branding: "ブランディング"
|
||||||
|
@ -1122,6 +1122,10 @@ authentication: "認証"
|
||||||
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
||||||
dateAndTime: "日時"
|
dateAndTime: "日時"
|
||||||
showRenotes: "リノートを表示"
|
showRenotes: "リノートを表示"
|
||||||
|
edited: "編集済み"
|
||||||
|
notificationRecieveConfig: "通知の受信設定"
|
||||||
|
mutualFollow: "相互フォロー"
|
||||||
|
fileAttachedOnly: "ファイル付きのみ"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -1145,7 +1149,7 @@ _initialAccountSetting:
|
||||||
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
|
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
|
||||||
initialAccountSettingCompleted: "初期設定が完了しました!"
|
initialAccountSettingCompleted: "初期設定が完了しました!"
|
||||||
haveFun: "{name}をお楽しみください!"
|
haveFun: "{name}をお楽しみください!"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
|
ifYouNeedLearnMore: "{name}(Sharkey)の使い方などを詳しく知るには{link}をご覧ください。"
|
||||||
skipAreYouSure: "初期設定をスキップしますか?"
|
skipAreYouSure: "初期設定をスキップしますか?"
|
||||||
laterAreYouSure: "初期設定をあとでやり直しますか?"
|
laterAreYouSure: "初期設定をあとでやり直しますか?"
|
||||||
|
|
||||||
|
@ -1170,7 +1174,7 @@ _accountMigration:
|
||||||
moveTo: "このアカウントを新しいアカウントへ移行"
|
moveTo: "このアカウントを新しいアカウントへ移行"
|
||||||
moveToLabel: "移行先のアカウント:"
|
moveToLabel: "移行先のアカウント:"
|
||||||
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできません。"
|
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできません。"
|
||||||
moveAccountDescription: "新しいアカウントへ移行します。\n ・フォロワーが新しいアカウントを自動でフォローします\n ・このアカウントからのフォローは全て解除されます\n ・このアカウントではノートの作成などができなくなります\n\nフォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。\nリスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。\n\n(この説明はこのサーバー(Misskey v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。)"
|
moveAccountDescription: "新しいアカウントへ移行します。\n ・フォロワーが新しいアカウントを自動でフォローします\n ・このアカウントからのフォローは全て解除されます\n ・このアカウントではノートの作成などができなくなります\n\nフォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。\nリスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。\n\n(この説明はこのサーバー(Sharkey v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。)"
|
||||||
moveAccountHowTo: "アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。\nエイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com"
|
moveAccountHowTo: "アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。\nエイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com"
|
||||||
startMigration: "移行する"
|
startMigration: "移行する"
|
||||||
migrationConfirm: "本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。"
|
migrationConfirm: "本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。"
|
||||||
|
@ -1184,7 +1188,7 @@ _achievements:
|
||||||
_notes1:
|
_notes1:
|
||||||
title: "just setting up my msky"
|
title: "just setting up my msky"
|
||||||
description: "初めてノートを投稿した"
|
description: "初めてノートを投稿した"
|
||||||
flavor: "良いMisskeyライフを!"
|
flavor: "良いSharkeyライフを!"
|
||||||
_notes10:
|
_notes10:
|
||||||
title: "いくつかのノート"
|
title: "いくつかのノート"
|
||||||
description: "ノートを10回投稿した"
|
description: "ノートを10回投稿した"
|
||||||
|
@ -1280,7 +1284,7 @@ _achievements:
|
||||||
_login1000:
|
_login1000:
|
||||||
title: "ノートマスターⅢ"
|
title: "ノートマスターⅢ"
|
||||||
description: "通算ログイン日数が1,000日"
|
description: "通算ログイン日数が1,000日"
|
||||||
flavor: "Misskeyを使ってくれてありがとう!"
|
flavor: "Sharkeyを使ってくれてありがとう!"
|
||||||
_noteClipped1:
|
_noteClipped1:
|
||||||
title: "クリップせずにはいられないな"
|
title: "クリップせずにはいられないな"
|
||||||
description: "初めてノートをクリップした"
|
description: "初めてノートをクリップした"
|
||||||
|
@ -1340,9 +1344,9 @@ _achievements:
|
||||||
title: "実績好き"
|
title: "実績好き"
|
||||||
description: "実績一覧を3分以上眺め続けた"
|
description: "実績一覧を3分以上眺め続けた"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "I Love Misskey"
|
title: "I Love Sharkey"
|
||||||
description: "\"I ❤ #Misskey\"を投稿した"
|
description: "\"I ❤ #Sharkey\"を投稿した"
|
||||||
flavor: "Misskeyを使ってくださりありがとうございます! by 開発チーム"
|
flavor: "Sharkeyを使ってくださりありがとうございます! by 開発チーム"
|
||||||
_foundTreasure:
|
_foundTreasure:
|
||||||
title: "宝探し"
|
title: "宝探し"
|
||||||
description: "隠されたお宝を発見した"
|
description: "隠されたお宝を発見した"
|
||||||
|
@ -1350,7 +1354,7 @@ _achievements:
|
||||||
title: "ひとやすみ"
|
title: "ひとやすみ"
|
||||||
description: "クライアントを起動してから30分以上経過した"
|
description: "クライアントを起動してから30分以上経過した"
|
||||||
_client60min:
|
_client60min:
|
||||||
title: "Misskeyの見すぎ"
|
title: "Sharkeyの見すぎ"
|
||||||
description: "クライアントを起動してから60分以上経過した"
|
description: "クライアントを起動してから60分以上経過した"
|
||||||
_noteDeletedWithin1min:
|
_noteDeletedWithin1min:
|
||||||
title: "いまのなし"
|
title: "いまのなし"
|
||||||
|
@ -1459,6 +1463,7 @@ _role:
|
||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
|
canEditNote: "ノートの編集"
|
||||||
canInvite: "サーバー招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
inviteLimit: "招待コードの作成可能数"
|
inviteLimit: "招待コードの作成可能数"
|
||||||
inviteLimitCycle: "招待コードの発行間隔"
|
inviteLimitCycle: "招待コードの発行間隔"
|
||||||
|
@ -1477,7 +1482,8 @@ _role:
|
||||||
rateLimitFactor: "レートリミット"
|
rateLimitFactor: "レートリミット"
|
||||||
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
||||||
canHideAds: "広告の非表示"
|
canHideAds: "広告の非表示"
|
||||||
canSearchNotes: "ノート検索の利用可否"
|
canSearchNotes: "ノート検索の利用"
|
||||||
|
canUseTranslator: "翻訳機能の利用"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
isRemote: "リモートユーザー"
|
isRemote: "リモートユーザー"
|
||||||
|
@ -1583,12 +1589,12 @@ _registry:
|
||||||
createKey: "キーを作成"
|
createKey: "キーを作成"
|
||||||
|
|
||||||
_aboutMisskey:
|
_aboutMisskey:
|
||||||
about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
|
about: "Sharkeyは、2014年からsyuiloによって開発されているMisskeyをベースにしたオープンソースのソフトウェアです。"
|
||||||
contributors: "主なコントリビューター"
|
contributors: "主なコントリビューター"
|
||||||
allContributors: "全てのコントリビューター"
|
allContributors: "全てのコントリビューター"
|
||||||
source: "ソースコード"
|
source: "ソースコード"
|
||||||
translation: "Misskeyを翻訳"
|
translation: "Sharkeyを翻訳"
|
||||||
donate: "Misskeyに寄付"
|
donate: "Sharkeyに寄付"
|
||||||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
||||||
patrons: "支援者"
|
patrons: "支援者"
|
||||||
|
|
||||||
|
@ -1746,7 +1752,7 @@ _time:
|
||||||
day: "日"
|
day: "日"
|
||||||
|
|
||||||
_timelineTutorial:
|
_timelineTutorial:
|
||||||
title: "Misskeyの使い方"
|
title: "Sharkeyの使い方"
|
||||||
step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。"
|
step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。"
|
||||||
step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。"
|
step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。"
|
||||||
step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||||
|
@ -2192,3 +2198,6 @@ _moderationLogTypes:
|
||||||
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
||||||
resolveAbuseReport: "通報を解決"
|
resolveAbuseReport: "通報を解決"
|
||||||
createInvitation: "招待コードを作成"
|
createInvitation: "招待コードを作成"
|
||||||
|
createAd: "広告を作成"
|
||||||
|
deleteAd: "広告を削除"
|
||||||
|
updateAd: "広告を更新"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
_lang_: "日本語 (関西弁)"
|
_lang_: "日本語 (関西弁)"
|
||||||
headlineMisskey: "ノートでつながるネットワーク"
|
headlineMisskey: "ノートでつながるネットワーク"
|
||||||
introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな、新しい世界を探検しよか🚀"
|
introMisskey: "ようお越し!Sharkeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな、新しい世界を探検しよか🚀"
|
||||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
|
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Sharkey</b>のサーバーのひとつなんやで。"
|
||||||
monthAndDay: "{month}月 {day}日"
|
monthAndDay: "{month}月 {day}日"
|
||||||
search: "探す"
|
search: "探す"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
|
@ -160,7 +160,7 @@ youCanCleanRemoteFilesCache: "ファイル管理にある🗑️ボタンでキ
|
||||||
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
||||||
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになるで。"
|
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになるで。"
|
||||||
flagAsBot: "Botにするで"
|
flagAsBot: "Botにするで"
|
||||||
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Sharkeyのシステム上での扱いがBotに合ったもんになるからな。"
|
||||||
flagAsCat: "Catやで"
|
flagAsCat: "Catやで"
|
||||||
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
||||||
|
@ -220,7 +220,7 @@ noUsers: "ユーザーはおらん"
|
||||||
editProfile: "プロフィールをいじる"
|
editProfile: "プロフィールをいじる"
|
||||||
noteDeleteConfirm: "このノートをほかしてええか?"
|
noteDeleteConfirm: "このノートをほかしてええか?"
|
||||||
pinLimitExceeded: "これ以上ピン留めできひん"
|
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||||
intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
|
intro: "Sharkeyのインストールが完了したで!管理者アカウントを作ってや。"
|
||||||
done: "でけた"
|
done: "でけた"
|
||||||
processing: "処理しとる"
|
processing: "処理しとる"
|
||||||
preview: "プレビュー"
|
preview: "プレビュー"
|
||||||
|
@ -406,7 +406,7 @@ exploreFediverse: "Fediverseを探ってみる"
|
||||||
popularTags: "人気のタグ"
|
popularTags: "人気のタグ"
|
||||||
userList: "リスト"
|
userList: "リスト"
|
||||||
about: "情報"
|
about: "情報"
|
||||||
aboutMisskey: "Misskeyってなんや?"
|
aboutMisskey: "Sharkeyってなんや?"
|
||||||
administrator: "管理者"
|
administrator: "管理者"
|
||||||
token: "トークン"
|
token: "トークン"
|
||||||
2fa: "二要素認証"
|
2fa: "二要素認証"
|
||||||
|
@ -552,7 +552,7 @@ sort: "仕分ける"
|
||||||
ascendingOrder: "小さい順"
|
ascendingOrder: "小さい順"
|
||||||
descendingOrder: "大きい順"
|
descendingOrder: "大きい順"
|
||||||
scratchpad: "スクラッチパッド"
|
scratchpad: "スクラッチパッド"
|
||||||
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
|
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Sharkeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
|
||||||
output: "出力"
|
output: "出力"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にしてや"
|
disablePagesScript: "Pagesのスクリプトを無効にしてや"
|
||||||
|
@ -683,7 +683,7 @@ unclip: "クリップ解除するで"
|
||||||
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?"
|
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?"
|
||||||
public: "パブリック"
|
public: "パブリック"
|
||||||
private: "非公開"
|
private: "非公開"
|
||||||
i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。"
|
i18nInfo: "Sharkeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。"
|
||||||
manageAccessTokens: "アクセストークンの管理"
|
manageAccessTokens: "アクセストークンの管理"
|
||||||
accountInfo: "アカウント情報"
|
accountInfo: "アカウント情報"
|
||||||
notesCount: "ノートの数やで"
|
notesCount: "ノートの数やで"
|
||||||
|
@ -736,7 +736,7 @@ onlineUsersCount: "{n}人が起きとるで"
|
||||||
nUsers: "{n}ユーザー"
|
nUsers: "{n}ユーザー"
|
||||||
nNotes: "{n}ノート"
|
nNotes: "{n}ノート"
|
||||||
sendErrorReports: "エラーリポートを送る"
|
sendErrorReports: "エラーリポートを送る"
|
||||||
sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がMisskeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。"
|
sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がSharkeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。"
|
||||||
myTheme: "マイテーマ"
|
myTheme: "マイテーマ"
|
||||||
backgroundColor: "背景"
|
backgroundColor: "背景"
|
||||||
accentColor: "アクセント"
|
accentColor: "アクセント"
|
||||||
|
@ -830,7 +830,7 @@ hashtags: "ハッシュタグ"
|
||||||
troubleshooting: "トラブルシューティング"
|
troubleshooting: "トラブルシューティング"
|
||||||
useBlurEffect: "UIにぼかし効果を使うで"
|
useBlurEffect: "UIにぼかし効果を使うで"
|
||||||
learnMore: "詳しく"
|
learnMore: "詳しく"
|
||||||
misskeyUpdated: "Misskeyが更新されたで!\nモデレーターの人らに感謝せなあかんで"
|
misskeyUpdated: "Sharkeyが更新されたで!\nモデレーターの人らに感謝せなあかんで"
|
||||||
whatIsNew: "更新情報を見るで"
|
whatIsNew: "更新情報を見るで"
|
||||||
translate: "翻訳"
|
translate: "翻訳"
|
||||||
translatedFrom: "{x}から翻訳するで"
|
translatedFrom: "{x}から翻訳するで"
|
||||||
|
@ -959,8 +959,8 @@ numberOfLikes: "いいね数"
|
||||||
show: "表示"
|
show: "表示"
|
||||||
neverShow: "今後表示しない"
|
neverShow: "今後表示しない"
|
||||||
remindMeLater: "また後で"
|
remindMeLater: "また後で"
|
||||||
didYouLikeMisskey: "Misskey気に入ってくれた?"
|
didYouLikeMisskey: "Sharkey気に入ってくれた?"
|
||||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
pleaseDonate: "Sharkeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
||||||
roles: "ロール"
|
roles: "ロール"
|
||||||
role: "ロール"
|
role: "ロール"
|
||||||
noRole: "ロールはありまへん"
|
noRole: "ロールはありまへん"
|
||||||
|
@ -1069,7 +1069,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロー
|
||||||
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
|
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
|
||||||
changeReactionConfirm: "ツッコミを別のに変えるか?"
|
changeReactionConfirm: "ツッコミを別のに変えるか?"
|
||||||
later: "あとで"
|
later: "あとで"
|
||||||
goToMisskey: "Misskeyへ"
|
goToMisskey: "Sharkeyへ"
|
||||||
additionalEmojiDictionary: "絵文字の追加辞書"
|
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
installed: "インストール済み"
|
installed: "インストール済み"
|
||||||
branding: "ブランディング"
|
branding: "ブランディング"
|
||||||
|
@ -1105,6 +1105,10 @@ youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんや
|
||||||
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
|
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
|
||||||
replies: "返事"
|
replies: "返事"
|
||||||
renotes: "Renote"
|
renotes: "Renote"
|
||||||
|
loadReplies: "返信を見るで"
|
||||||
|
loadConversation: "会話を見るで"
|
||||||
|
verifiedLink: "このリンク先の所有者であることが確認されたで。"
|
||||||
|
authenticationRequiredToContinue: "続けるには認証をやってや。"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "もうおるユーザーのみ"
|
forExistingUsers: "もうおるユーザーのみ"
|
||||||
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
||||||
|
@ -1126,13 +1130,18 @@ _initialAccountSetting:
|
||||||
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。"
|
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。"
|
||||||
initialAccountSettingCompleted: "初期設定が終わったで。"
|
initialAccountSettingCompleted: "初期設定が終わったで。"
|
||||||
haveFun: "{name}、楽しんでな~"
|
haveFun: "{name}、楽しんでな~"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
ifYouNeedLearnMore: "{name}(Sharkey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
||||||
skipAreYouSure: "初期設定飛ばすか?"
|
skipAreYouSure: "初期設定飛ばすか?"
|
||||||
laterAreYouSure: "初期設定あとでやり直すん?"
|
laterAreYouSure: "初期設定あとでやり直すん?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "アイコン画像のURL"
|
iconUrl: "アイコン画像のURL"
|
||||||
|
appIconDescription: "{host}がアプリとして表示してるんやつをアイコンを指定すんで。"
|
||||||
|
appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
|
||||||
|
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。"
|
||||||
|
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。"
|
||||||
|
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
||||||
moveFromSub: "別のアカウントへエイリアスを作る"
|
moveFromSub: "別のアカウントへエイリアスを作る"
|
||||||
|
@ -1141,7 +1150,7 @@ _accountMigration:
|
||||||
moveTo: "このアカウントをさらのアカウントに引っ越すで"
|
moveTo: "このアカウントをさらのアカウントに引っ越すで"
|
||||||
moveToLabel: "引っ越し先のアカウント:"
|
moveToLabel: "引っ越し先のアカウント:"
|
||||||
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできへんくなります。"
|
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできへんくなります。"
|
||||||
moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでノート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\n(この説明はこのサーバー、つまりMisskey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。)"
|
moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでノート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\n(この説明はこのサーバー、つまりSharkey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。)"
|
||||||
moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作成しなはれや。\nエイリアス作成した後、引っ越し先のアカウントを次のように入力してくれへんか?:@username@server.example.com"
|
moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作成しなはれや。\nエイリアス作成した後、引っ越し先のアカウントを次のように入力してくれへんか?:@username@server.example.com"
|
||||||
startMigration: "引っ越しする"
|
startMigration: "引っ越しする"
|
||||||
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃ~んと確認しーや?"
|
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃ~んと確認しーや?"
|
||||||
|
@ -1154,7 +1163,7 @@ _achievements:
|
||||||
_notes1:
|
_notes1:
|
||||||
title: "まいど!"
|
title: "まいど!"
|
||||||
description: "初めてノート投稿したった"
|
description: "初めてノート投稿したった"
|
||||||
flavor: "Misskeyを楽しんでな~"
|
flavor: "Sharkeyを楽しんでな~"
|
||||||
_notes10:
|
_notes10:
|
||||||
title: "ノートの天保山"
|
title: "ノートの天保山"
|
||||||
description: "ノートを10回投稿した"
|
description: "ノートを10回投稿した"
|
||||||
|
@ -1250,7 +1259,7 @@ _achievements:
|
||||||
_login1000:
|
_login1000:
|
||||||
title: "ノートマイスターⅢ"
|
title: "ノートマイスターⅢ"
|
||||||
description: "通算1,000日ログインした"
|
description: "通算1,000日ログインした"
|
||||||
flavor: "Misskeyようさん使てもろておおきにな!"
|
flavor: "Sharkeyようさん使てもろておおきにな!"
|
||||||
_noteClipped1:
|
_noteClipped1:
|
||||||
title: "アカンどれもクリップしたいわ"
|
title: "アカンどれもクリップしたいわ"
|
||||||
description: "初めてノートをクリップした"
|
description: "初めてノートをクリップした"
|
||||||
|
@ -1310,9 +1319,9 @@ _achievements:
|
||||||
title: "実績好き"
|
title: "実績好き"
|
||||||
description: "実績一覧を3分以上眺め続けた"
|
description: "実績一覧を3分以上眺め続けた"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "Misskey好きやねん"
|
title: "Sharkey好きやねん"
|
||||||
description: "\"I ❤ #Misskey\"を投稿した"
|
description: "\"I ❤ #Sharkey\"を投稿した"
|
||||||
flavor: "Misskeyを使ってくれておおきにな~ by 開発チーム"
|
flavor: "Sharkeyを使ってくれておおきにな~ by 開発チーム"
|
||||||
_foundTreasure:
|
_foundTreasure:
|
||||||
title: "なんでも鑑定団"
|
title: "なんでも鑑定団"
|
||||||
description: "隠されたお宝を発見した"
|
description: "隠されたお宝を発見した"
|
||||||
|
@ -1320,7 +1329,7 @@ _achievements:
|
||||||
title: "ねんね"
|
title: "ねんね"
|
||||||
description: "クライアントを起動してから30分以上経過した"
|
description: "クライアントを起動してから30分以上経過した"
|
||||||
_client60min:
|
_client60min:
|
||||||
title: "Misskeyの見過ぎや!"
|
title: "Sharkeyの見過ぎや!"
|
||||||
description: "クライアント付けてから1時間経ってもうたで。"
|
description: "クライアント付けてから1時間経ってもうたで。"
|
||||||
_noteDeletedWithin1min:
|
_noteDeletedWithin1min:
|
||||||
title: "*おおっと*"
|
title: "*おおっと*"
|
||||||
|
@ -1536,12 +1545,12 @@ _registry:
|
||||||
domain: "ドメイン"
|
domain: "ドメイン"
|
||||||
createKey: "キーを作る"
|
createKey: "キーを作る"
|
||||||
_aboutMisskey:
|
_aboutMisskey:
|
||||||
about: "Misskeyはsyuiloが2014年からずっと作ってはる、オープンソースなソフトウェアや。"
|
about: "Sharkeyは、syuiloが2014年からずっと作ってはる、Misskeyをベースにしたオープンソースなソフトウェアや。"
|
||||||
contributors: "主な貢献者"
|
contributors: "主な貢献者"
|
||||||
allContributors: "全ての貢献者"
|
allContributors: "全ての貢献者"
|
||||||
source: "ソースコード"
|
source: "ソースコード"
|
||||||
translation: "Misskeyを翻訳"
|
translation: "Sharkeyを翻訳"
|
||||||
donate: "Misskeyに寄付"
|
donate: "Sharkeyに寄付"
|
||||||
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
|
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
|
||||||
patrons: "支援者"
|
patrons: "支援者"
|
||||||
_displayOfSensitiveMedia:
|
_displayOfSensitiveMedia:
|
||||||
|
@ -1686,7 +1695,7 @@ _time:
|
||||||
hour: "時間"
|
hour: "時間"
|
||||||
day: "日"
|
day: "日"
|
||||||
_timelineTutorial:
|
_timelineTutorial:
|
||||||
title: "Misskeyってなんや?"
|
title: "Sharkeyってなんや?"
|
||||||
step1_1: "これは「タイムライン」や。{name}に投稿された「ノート」が順番に表示されるで。"
|
step1_1: "これは「タイムライン」や。{name}に投稿された「ノート」が順番に表示されるで。"
|
||||||
step1_2: "タイムラインには何個か種類があってな、例えば「ホームタイムライン」だったらあんたのフォローしてる人のノート、「ローカルタイムライン」には{name}全部のノートが流れてくるで。"
|
step1_2: "タイムラインには何個か種類があってな、例えば「ホームタイムライン」だったらあんたのフォローしてる人のノート、「ローカルタイムライン」には{name}全部のノートが流れてくるで。"
|
||||||
step2_1: "試しに、何かノートを投稿してみ。画面の鉛筆マークのボタンでフォームが開くで。"
|
step2_1: "試しに、何かノートを投稿してみ。画面の鉛筆マークのボタンでフォームが開くで。"
|
||||||
|
@ -1703,6 +1712,7 @@ _2fa:
|
||||||
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
|
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
|
||||||
step3Title: "確認コードを入れてーや"
|
step3Title: "確認コードを入れてーや"
|
||||||
step3: "アプリに表示されているトークンを入力して終わりや。"
|
step3: "アプリに表示されているトークンを入力して終わりや。"
|
||||||
|
setupCompleted: "設定が完了したで。"
|
||||||
step4: "これからログインするときも、同じようにトークンを入力するんやで"
|
step4: "これからログインするときも、同じようにトークンを入力するんやで"
|
||||||
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
|
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
|
||||||
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
|
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
|
||||||
|
@ -1717,6 +1727,10 @@ _2fa:
|
||||||
renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?"
|
renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?"
|
||||||
renewTOTPOk: "もっかい設定する"
|
renewTOTPOk: "もっかい設定する"
|
||||||
renewTOTPCancel: "やめとく"
|
renewTOTPCancel: "やめとく"
|
||||||
|
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。"
|
||||||
|
backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。"
|
||||||
|
backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。"
|
||||||
|
backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。"
|
||||||
_permissions:
|
_permissions:
|
||||||
"read:account": "アカウントの情報を見るで"
|
"read:account": "アカウントの情報を見るで"
|
||||||
"write:account": "アカウントの情報を変更するで"
|
"write:account": "アカウントの情報を変更するで"
|
||||||
|
@ -1989,6 +2003,9 @@ _notification:
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
|
||||||
achievementEarned: "実績を獲得しとるで"
|
achievementEarned: "実績を獲得しとるで"
|
||||||
|
checkNotificationBehavior: "通知の表示を確かめるで"
|
||||||
|
sendTestNotification: "テスト通知を送信するで"
|
||||||
|
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
follow: "フォロー"
|
follow: "フォロー"
|
||||||
|
@ -2024,6 +2041,7 @@ _deck:
|
||||||
introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。"
|
introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。"
|
||||||
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
|
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
|
||||||
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
||||||
|
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで"
|
||||||
_columns:
|
_columns:
|
||||||
main: "メイン"
|
main: "メイン"
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
|
@ -2061,3 +2079,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
resetPassword: "パスワードをリセット"
|
resetPassword: "パスワードをリセット"
|
||||||
|
createInvitation: "招待コードを作成"
|
||||||
|
|
|
@ -416,6 +416,9 @@ totp: "인증 앱"
|
||||||
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
|
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
|
||||||
moderator: "모더레이터"
|
moderator: "모더레이터"
|
||||||
moderation: "모더레이션"
|
moderation: "모더레이션"
|
||||||
|
moderationNote: "모더레이션 노트"
|
||||||
|
addModerationNote: "모더레이션 노트 추가하기"
|
||||||
|
moderationLogs: "모더레이션 로그"
|
||||||
nUsersMentioned: "{n}명이 언급함"
|
nUsersMentioned: "{n}명이 언급함"
|
||||||
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
||||||
securityKey: "보안 키"
|
securityKey: "보안 키"
|
||||||
|
@ -1107,6 +1110,18 @@ youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있습니다."
|
||||||
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오."
|
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오."
|
||||||
replies: "답글"
|
replies: "답글"
|
||||||
renotes: "리노트"
|
renotes: "리노트"
|
||||||
|
loadReplies: "답글 보기"
|
||||||
|
loadConversation: "대화 보기"
|
||||||
|
pinnedList: "고정해놓은 리스트"
|
||||||
|
keepScreenOn: "기기 화면을 항상 켜기"
|
||||||
|
verifiedLink: "이 링크의 소유자임이 확인되었습니다."
|
||||||
|
notifyNotes: "새 노트 알림 켜기"
|
||||||
|
unnotifyNotes: "새 노트 알림 끄기"
|
||||||
|
authentication: "인증"
|
||||||
|
showRenotes: "리노트 표시"
|
||||||
|
edited: "수정됨"
|
||||||
|
notificationRecieveConfig: "알림 설정"
|
||||||
|
mutualFollow: "맞팔로우"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "기존 유저에게만 알림"
|
forExistingUsers: "기존 유저에게만 알림"
|
||||||
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
|
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
|
||||||
|
@ -1135,6 +1150,12 @@ _serverRules:
|
||||||
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "아이콘 URL"
|
iconUrl: "아이콘 URL"
|
||||||
|
appIconUsageExample: "예를 들어, PWA나 스마트폰 홈 화면에 북마크로 추가되었을 때 등"
|
||||||
|
appIconStyleRecommendation: "아이콘이 원형 또는 둥근 사각형으로 잘리는 경우가 있으므로, 가장자리 여백이 충분한 사진을 사용하는 것을 추천합니다."
|
||||||
|
appIconResolutionMustBe: "해상도는 반드시 {resolution} 이어야 합니다."
|
||||||
|
manifestJsonOverride: "manifest.json 오버라이드"
|
||||||
|
shortName: "약칭"
|
||||||
|
shortNameDescription: "서버의 정식 명칭이 긴 경우에, 대신에 표시할 수 있는 약칭이나 통칭."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
||||||
|
@ -2076,3 +2097,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "정지"
|
suspend: "정지"
|
||||||
resetPassword: "비밀번호 재설정"
|
resetPassword: "비밀번호 재설정"
|
||||||
|
createInvitation: "초대 코드 생성"
|
||||||
|
|
|
@ -416,6 +416,9 @@ totp: "แอป Authenticator"
|
||||||
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
|
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
|
||||||
moderator: "ผู้ควบคุม"
|
moderator: "ผู้ควบคุม"
|
||||||
moderation: "การกลั่นกรอง"
|
moderation: "การกลั่นกรอง"
|
||||||
|
moderationNote: "โน้ตการกลั่นกรอง"
|
||||||
|
addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
|
||||||
|
moderationLogs: "บันทึกการกลั่นกรอง"
|
||||||
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
|
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
|
||||||
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
|
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
|
||||||
securityKey: "กุญแจความปลอดภัย"
|
securityKey: "กุญแจความปลอดภัย"
|
||||||
|
@ -708,6 +711,7 @@ lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องต
|
||||||
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
|
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
|
||||||
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
||||||
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
||||||
|
highlightSensitiveMedia: "ไฮไลท์สื่อที่ละเอียดอ่อน"
|
||||||
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
|
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
|
||||||
notSet: "ไม่ได้ตั้งค่า"
|
notSet: "ไม่ได้ตั้งค่า"
|
||||||
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
|
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
|
||||||
|
@ -1022,6 +1026,7 @@ retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการ
|
||||||
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
||||||
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
|
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
|
||||||
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
|
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
|
||||||
|
reactionsDisplaySize: "รีแอคชั่นแสดงผลขนาด"
|
||||||
noteIdOrUrl: "โน้ต ID หรือ URL"
|
noteIdOrUrl: "โน้ต ID หรือ URL"
|
||||||
video: "วีดีโอ"
|
video: "วีดีโอ"
|
||||||
videos: "วีดีโอ"
|
videos: "วีดีโอ"
|
||||||
|
@ -1100,11 +1105,26 @@ iHaveReadXCarefullyAndAgree: "ฉันได้อ่านข้อควา
|
||||||
dialog: "ไดอะล็อก"
|
dialog: "ไดอะล็อก"
|
||||||
icon: "ไอคอน"
|
icon: "ไอคอน"
|
||||||
forYou: "สำหรับคุณ"
|
forYou: "สำหรับคุณ"
|
||||||
|
currentAnnouncements: "ประกาศในปัจจุบัน"
|
||||||
|
pastAnnouncements: "ประกาศที่ผ่านมา"
|
||||||
|
youHaveUnreadAnnouncements: "มีการประกาศที่ยังไม่ได้อ่าน"
|
||||||
replies: "ตอบกลับ"
|
replies: "ตอบกลับ"
|
||||||
renotes: "รีโน้ต"
|
renotes: "รีโน้ต"
|
||||||
loadReplies: "แสดงการตอบกลับ"
|
loadReplies: "แสดงการตอบกลับ"
|
||||||
loadConversation: "แสดงบทสนทนา"
|
loadConversation: "แสดงบทสนทนา"
|
||||||
|
pinnedList: "รายการที่ปักหมุดไว้แล้ว"
|
||||||
|
keepScreenOn: "เปิดหน้าจอไว้"
|
||||||
|
notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่"
|
||||||
|
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
||||||
|
authentication: "การตรวจสอบสิทธิ์"
|
||||||
|
dateAndTime: "เวลาประทับ"
|
||||||
|
showRenotes: "แสดงรีโน้ต"
|
||||||
|
edited: "แก้ไขแล้ว"
|
||||||
|
notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
|
||||||
|
mutualFollow: "ติดตามซึ่งกันและกัน"
|
||||||
|
fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น"
|
||||||
_announcement:
|
_announcement:
|
||||||
|
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
|
||||||
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
||||||
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
||||||
end: "ประกาศเก็บถาวร"
|
end: "ประกาศเก็บถาวร"
|
||||||
|
@ -1130,6 +1150,7 @@ _serverRules:
|
||||||
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "ไอคอน URL"
|
iconUrl: "ไอคอน URL"
|
||||||
|
shortName: "ชื่อย่อ"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
||||||
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
|
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
|
||||||
|
@ -1424,6 +1445,7 @@ _role:
|
||||||
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
|
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
|
||||||
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
|
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
|
||||||
canPublicNote: "สามารถส่งโน้ตสาธารณะ"
|
canPublicNote: "สามารถส่งโน้ตสาธารณะ"
|
||||||
|
canEditNote: "กำลังแก้ไขโน้ต"
|
||||||
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
||||||
inviteLimit: "จำกัดการเชิญ"
|
inviteLimit: "จำกัดการเชิญ"
|
||||||
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
|
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
|
||||||
|
@ -1987,6 +2009,7 @@ _notification:
|
||||||
youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ"
|
youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ"
|
||||||
yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ"
|
yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ"
|
||||||
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
||||||
|
newNote: "โพสต์ใหม่"
|
||||||
unreadAntennaNote: "เสาอากาศ {name}"
|
unreadAntennaNote: "เสาอากาศ {name}"
|
||||||
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
||||||
achievementEarned: "รับความสำเร็จ"
|
achievementEarned: "รับความสำเร็จ"
|
||||||
|
@ -1996,6 +2019,7 @@ _notification:
|
||||||
notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้"
|
notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้"
|
||||||
_types:
|
_types:
|
||||||
all: "ทั้งหมด"
|
all: "ทั้งหมด"
|
||||||
|
note: "โน้ตใหม่"
|
||||||
follow: "กำลังติดตาม"
|
follow: "กำลังติดตาม"
|
||||||
mention: "กล่าวถึง"
|
mention: "กล่าวถึง"
|
||||||
reply: "ตอบกลับ"
|
reply: "ตอบกลับ"
|
||||||
|
@ -2066,5 +2090,23 @@ _webhookSettings:
|
||||||
reaction: "เมื่อได้รับรีแอคชั่น"
|
reaction: "เมื่อได้รับรีแอคชั่น"
|
||||||
mention: "เมื่อกำลังถูกกล่าวถึง"
|
mention: "เมื่อกำลังถูกกล่าวถึง"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "สร้างบทบาทแล้ว"
|
||||||
|
deleteRole: "ลบบทบาทแล้ว"
|
||||||
|
updateRole: "อัปเดตบทบาทแล้ว"
|
||||||
|
assignRole: "ได้รับมอบหมายบทบาท"
|
||||||
|
unassignRole: "ถอดออกจากบทบาทแล้ว"
|
||||||
suspend: "ถูกระงับ"
|
suspend: "ถูกระงับ"
|
||||||
|
unsuspend: "เลิกถูกระงับ"
|
||||||
|
addCustomEmoji: "เพิ่มอีโมจิที่กำหนดเองแล้ว"
|
||||||
|
updateCustomEmoji: "อัปเดตอีโมจิที่กำหนดเองแล้ว"
|
||||||
|
deleteCustomEmoji: "ลบอีโมจิที่กำหนดเองออกแล้ว"
|
||||||
|
updateServerSettings: "อัปเดตการตั้งค่าเซิร์ฟเวอร์แล้ว"
|
||||||
|
updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว"
|
||||||
|
deleteDriveFile: "ลบไฟล์ออกแล้ว"
|
||||||
|
deleteNote: "ลบโน้ตออกแล้ว"
|
||||||
resetPassword: "รีเซ็ตรหัสผ่าน"
|
resetPassword: "รีเซ็ตรหัสผ่าน"
|
||||||
|
resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
|
||||||
|
createInvitation: "สร้างคำเชิญ"
|
||||||
|
createAd: "สร้างโฆษณาแล้ว"
|
||||||
|
deleteAd: "ลบโฆษณาออกแล้ว"
|
||||||
|
updateAd: "อัปเดตโฆษณาแล้ว"
|
||||||
|
|
|
@ -1120,6 +1120,12 @@ notifyNotes: "打开发帖通知"
|
||||||
unnotifyNotes: "关闭发帖通知"
|
unnotifyNotes: "关闭发帖通知"
|
||||||
authentication: "验证"
|
authentication: "验证"
|
||||||
authenticationRequiredToContinue: "要继续,请先进行验证"
|
authenticationRequiredToContinue: "要继续,请先进行验证"
|
||||||
|
dateAndTime: "日期和时间"
|
||||||
|
showRenotes: "显示转帖"
|
||||||
|
edited: "已编辑"
|
||||||
|
notificationRecieveConfig: "通知接收设置"
|
||||||
|
mutualFollow: "互相关注"
|
||||||
|
fileAttachedOnly: "仅限媒体"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "仅限现有用户"
|
forExistingUsers: "仅限现有用户"
|
||||||
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
|
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
|
||||||
|
@ -1450,6 +1456,7 @@ _role:
|
||||||
gtlAvailable: "查看全局时间线"
|
gtlAvailable: "查看全局时间线"
|
||||||
ltlAvailable: "查看本地时间线"
|
ltlAvailable: "查看本地时间线"
|
||||||
canPublicNote: "允许公开发帖"
|
canPublicNote: "允许公开发帖"
|
||||||
|
canEditNote: "编辑帖子"
|
||||||
canInvite: "发放服务器邀请码"
|
canInvite: "发放服务器邀请码"
|
||||||
inviteLimit: "可发行邀请码的数量"
|
inviteLimit: "可发行邀请码的数量"
|
||||||
inviteLimitCycle: "邀请码的发行间隔"
|
inviteLimitCycle: "邀请码的发行间隔"
|
||||||
|
@ -2101,6 +2108,8 @@ _webhookSettings:
|
||||||
reaction: "被回应时"
|
reaction: "被回应时"
|
||||||
mention: "被提及时"
|
mention: "被提及时"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
|
createRole: "创建角色"
|
||||||
|
deleteRole: "删除角色"
|
||||||
updateRole: "更新角色"
|
updateRole: "更新角色"
|
||||||
assignRole: "分配角色"
|
assignRole: "分配角色"
|
||||||
unassignRole: "取消分配角色"
|
unassignRole: "取消分配角色"
|
||||||
|
@ -2122,3 +2131,8 @@ _moderationLogTypes:
|
||||||
resetPassword: "重置密码"
|
resetPassword: "重置密码"
|
||||||
markSensitiveDriveFile: "标记网盘文件为敏感媒体"
|
markSensitiveDriveFile: "标记网盘文件为敏感媒体"
|
||||||
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
|
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
|
||||||
|
resolveAbuseReport: "处理举报"
|
||||||
|
createInvitation: "发行邀请码"
|
||||||
|
createAd: "创建了广告"
|
||||||
|
deleteAd: "删除了广告"
|
||||||
|
updateAd: "更新了广告"
|
||||||
|
|
|
@ -15,7 +15,7 @@ gotIt: "知道了"
|
||||||
cancel: "取消"
|
cancel: "取消"
|
||||||
noThankYou: "現在不要"
|
noThankYou: "現在不要"
|
||||||
enterUsername: "輸入使用者名稱"
|
enterUsername: "輸入使用者名稱"
|
||||||
renotedBy: "{user} 轉發"
|
renotedBy: "{user} 轉發了"
|
||||||
noNotes: "無貼文"
|
noNotes: "無貼文"
|
||||||
noNotifications: "沒有通知"
|
noNotifications: "沒有通知"
|
||||||
instance: "伺服器"
|
instance: "伺服器"
|
||||||
|
@ -75,9 +75,9 @@ import: "匯入"
|
||||||
export: "匯出"
|
export: "匯出"
|
||||||
files: "檔案"
|
files: "檔案"
|
||||||
download: "下載"
|
download: "下載"
|
||||||
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
|
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此檔案的貼文也會跟著被刪除。"
|
||||||
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
||||||
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端裡。"
|
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端硬碟裡。"
|
||||||
importRequested: "已請求匯入。這可能會花一點時間。"
|
importRequested: "已請求匯入。這可能會花一點時間。"
|
||||||
lists: "清單"
|
lists: "清單"
|
||||||
noLists: "你沒有任何清單"
|
noLists: "你沒有任何清單"
|
||||||
|
@ -156,16 +156,16 @@ emojiUrl: "表情符號 URL"
|
||||||
addEmoji: "新增表情符號"
|
addEmoji: "新增表情符號"
|
||||||
settingGuide: "推薦設定"
|
settingGuide: "推薦設定"
|
||||||
cacheRemoteFiles: "快取遠端檔案"
|
cacheRemoteFiles: "快取遠端檔案"
|
||||||
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但為了產生圖片的縮圖並保護使用者的隱私,建議將 default.yml 的 proxyRemoteFiles 設為 true。"
|
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私,。"
|
||||||
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
|
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
|
||||||
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
|
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
|
||||||
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
|
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
|
||||||
flagAsBot: "此使用者是機器人"
|
flagAsBot: "此使用者是機器人"
|
||||||
flagAsBotDescription: "標記本帳戶由程式控制,防止其他程式與本帳戶產生無限互動的行為。"
|
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人"
|
||||||
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
|
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
|
||||||
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
|
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
|
||||||
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
||||||
flagShowTimelineRepliesDescription: "啟用時,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
|
flagShowTimelineRepliesDescription: "啟用後,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
|
||||||
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
|
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
|
||||||
addAccount: "新增帳戶"
|
addAccount: "新增帳戶"
|
||||||
reloadAccountsList: "更新帳戶清單的資訊"
|
reloadAccountsList: "更新帳戶清單的資訊"
|
||||||
|
@ -184,7 +184,7 @@ host: "主機"
|
||||||
selectUser: "選取使用者"
|
selectUser: "選取使用者"
|
||||||
recipient: "收件人"
|
recipient: "收件人"
|
||||||
annotation: "註解"
|
annotation: "註解"
|
||||||
federation: "聯邦宇宙"
|
federation: "站台聯邦"
|
||||||
instances: "伺服器"
|
instances: "伺服器"
|
||||||
registeredAt: "初次觀測"
|
registeredAt: "初次觀測"
|
||||||
latestRequestReceivedAt: "上次收到的請求"
|
latestRequestReceivedAt: "上次收到的請求"
|
||||||
|
@ -573,7 +573,7 @@ tokenRevokedDescription: "登入權杖失效,請重新登入。"
|
||||||
accountDeleted: "帳戶已被刪除"
|
accountDeleted: "帳戶已被刪除"
|
||||||
accountDeletedDescription: "這個帳戶已被刪除。"
|
accountDeletedDescription: "這個帳戶已被刪除。"
|
||||||
menu: "選單"
|
menu: "選單"
|
||||||
divider: "分割線"
|
divider: "分隔線"
|
||||||
addItem: "新增項目"
|
addItem: "新增項目"
|
||||||
rearrange: "排序方式"
|
rearrange: "排序方式"
|
||||||
relays: "中繼"
|
relays: "中繼"
|
||||||
|
@ -582,7 +582,7 @@ inboxUrl: "收件夾URL"
|
||||||
addedRelays: "已加入的中繼"
|
addedRelays: "已加入的中繼"
|
||||||
serviceworkerInfo: "您需要啟用推送通知。"
|
serviceworkerInfo: "您需要啟用推送通知。"
|
||||||
deletedNote: "已刪除的貼文"
|
deletedNote: "已刪除的貼文"
|
||||||
invisibleNote: "隱藏的貼文"
|
invisibleNote: "私密的貼文"
|
||||||
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
||||||
visibility: "可見性"
|
visibility: "可見性"
|
||||||
poll: "投票"
|
poll: "投票"
|
||||||
|
@ -1121,6 +1121,9 @@ unnotifyNotes: "關閉貼文通知"
|
||||||
authentication: "驗證"
|
authentication: "驗證"
|
||||||
authenticationRequiredToContinue: "請於繼續前完成驗證"
|
authenticationRequiredToContinue: "請於繼續前完成驗證"
|
||||||
dateAndTime: "日期與時間"
|
dateAndTime: "日期與時間"
|
||||||
|
showRenotes: "顯示轉發貼文"
|
||||||
|
edited: "已編輯"
|
||||||
|
mutualFollow: "互相追隨"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "僅限既有的使用者"
|
forExistingUsers: "僅限既有的使用者"
|
||||||
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
|
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
|
||||||
|
@ -1451,6 +1454,7 @@ _role:
|
||||||
gtlAvailable: "瀏覽全域時間軸"
|
gtlAvailable: "瀏覽全域時間軸"
|
||||||
ltlAvailable: "瀏覽本地時間軸"
|
ltlAvailable: "瀏覽本地時間軸"
|
||||||
canPublicNote: "允許公開貼文"
|
canPublicNote: "允許公開貼文"
|
||||||
|
canEditNote: "允許編輯貼文"
|
||||||
canInvite: "發行實例邀請碼"
|
canInvite: "發行實例邀請碼"
|
||||||
inviteLimit: "可建立邀請碼的數量"
|
inviteLimit: "可建立邀請碼的數量"
|
||||||
inviteLimitCycle: "邀請碼的發放間隔"
|
inviteLimitCycle: "邀請碼的發放間隔"
|
||||||
|
@ -1576,8 +1580,8 @@ _displayOfSensitiveMedia:
|
||||||
force: "隱藏所有檔案"
|
force: "隱藏所有檔案"
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
none: "隱藏"
|
none: "隱藏"
|
||||||
remote: "向遠端使用者顯示"
|
remote: "只顯示遠端使用者"
|
||||||
always: "總是顯示"
|
always: "一律顯示"
|
||||||
_serverDisconnectedBehavior:
|
_serverDisconnectedBehavior:
|
||||||
reload: "自動重載"
|
reload: "自動重載"
|
||||||
dialog: "彈出式警告"
|
dialog: "彈出式警告"
|
||||||
|
@ -1610,7 +1614,7 @@ _wordMute:
|
||||||
mutedNotes: "已靜音的貼文"
|
mutedNotes: "已靜音的貼文"
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
|
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
|
||||||
instanceMuteDescription2: "換行以分隔"
|
instanceMuteDescription2: "設定時以換行進行分隔"
|
||||||
title: "將隱藏被設定的實例貼文。"
|
title: "將隱藏被設定的實例貼文。"
|
||||||
heading: "將實例靜音"
|
heading: "將實例靜音"
|
||||||
_theme:
|
_theme:
|
||||||
|
@ -1663,7 +1667,7 @@ _theme:
|
||||||
mentionMe: "提到了我"
|
mentionMe: "提到了我"
|
||||||
renote: "轉發貼文"
|
renote: "轉發貼文"
|
||||||
modalBg: "對話框背景"
|
modalBg: "對話框背景"
|
||||||
divider: "分割線"
|
divider: "分隔線"
|
||||||
scrollbarHandle: "捲動條"
|
scrollbarHandle: "捲動條"
|
||||||
scrollbarHandleHover: "捲動條(懸浮)"
|
scrollbarHandleHover: "捲動條(懸浮)"
|
||||||
dateLabelFg: "日期標籤文字"
|
dateLabelFg: "日期標籤文字"
|
||||||
|
@ -2127,3 +2131,8 @@ _moderationLogTypes:
|
||||||
unsuspendRemoteInstance: "解除封鎖遠端伺服器"
|
unsuspendRemoteInstance: "解除封鎖遠端伺服器"
|
||||||
markSensitiveDriveFile: "標記為敏感檔案"
|
markSensitiveDriveFile: "標記為敏感檔案"
|
||||||
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
|
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
|
||||||
|
resolveAbuseReport: "解決檢舉"
|
||||||
|
createInvitation: "建立邀請碼"
|
||||||
|
createAd: "建立廣告"
|
||||||
|
deleteAd: "刪除廣告"
|
||||||
|
updateAd: "更新廣告"
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2023.9.1.beta3",
|
"version": "2023.9.1.beta4",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/transfem-org/sharkey.git"
|
"url": "https://github.com/transfem-org/sharkey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.7.6",
|
"packageManager": "pnpm@8.8.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
|
@ -46,15 +46,15 @@
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.30",
|
"postcss": "8.4.31",
|
||||||
"terser": "5.20.0",
|
"terser": "5.20.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.3",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.2.0",
|
"cypress": "13.3.0",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
"start-server-and-test": "2.0.1"
|
"start-server-and-test": "2.0.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export class largerImageComment1680969937000 {
|
||||||
|
name = 'largerImageComment1680969937000';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE character varying(8192)`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE character varying(512)`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey 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 '{}'`);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1696332072038-clean.js
Normal file
18
packages/backend/migration/1696332072038-clean.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export class Clean1696332072038 {
|
||||||
|
name = 'Clean1696332072038'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`);
|
||||||
|
}
|
||||||
|
}
|
12
packages/backend/migration/1696386694000-speakAsCat.js
Normal file
12
packages/backend/migration/1696386694000-speakAsCat.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export class SpeakAsCat1696386694000 {
|
||||||
|
name = "SpeakAsCat1696386694000";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "speakAsCat" boolean NOT NULL DEFAULT true`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."speakAsCat" IS 'Whether to speak as a cat if chosen.'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "speakAsCat"`);
|
||||||
|
}
|
||||||
|
}
|
19
packages/backend/migration/1696548899000-background.js
Normal file
19
packages/backend/migration/1696548899000-background.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export class Background1696548899000 {
|
||||||
|
name = 'Background1696548899000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundId" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5" UNIQUE ("backgroundId")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n" FOREIGN KEY ("backgroundId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundUrl" character varying(512)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundBlurhash" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundBlurhash"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||||
"@tensorflow/tfjs": "4.4.0",
|
"@tensorflow/tfjs": "4.4.0",
|
||||||
"@tensorflow/tfjs-node": "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-arm-eabi": "0.0.10",
|
||||||
"slacc-android-arm64": "0.0.10",
|
"slacc-android-arm64": "0.0.10",
|
||||||
"slacc-darwin-arm64": "0.0.10",
|
"slacc-darwin-arm64": "0.0.10",
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
"slacc-linux-x64-musl": "0.0.10",
|
"slacc-linux-x64-musl": "0.0.10",
|
||||||
"slacc-win32-arm64-msvc": "0.0.10",
|
"slacc-win32-arm64-msvc": "0.0.10",
|
||||||
"slacc-win32-x64-msvc": "0.0.10",
|
"slacc-win32-x64-msvc": "0.0.10",
|
||||||
"utf-8-validate": "^6.0.3"
|
"utf-8-validate": "6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.412.0",
|
||||||
|
@ -67,18 +67,18 @@
|
||||||
"@fastify/cors": "8.4.0",
|
"@fastify/cors": "8.4.0",
|
||||||
"@fastify/express": "2.3.0",
|
"@fastify/express": "2.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.2.1",
|
||||||
"@fastify/multipart": "7.7.3",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.11.2",
|
"@fastify/static": "6.11.2",
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@nestjs/common": "10.2.6",
|
"@nestjs/common": "10.2.6",
|
||||||
"@nestjs/core": "10.2.6",
|
"@nestjs/core": "10.2.6",
|
||||||
"@nestjs/testing": "10.2.6",
|
"@nestjs/testing": "10.2.6",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "8.1.1",
|
"@simplewebauthn/server": "8.2.0",
|
||||||
"@sinonjs/fake-timers": "11.1.0",
|
"@sinonjs/fake-timers": "11.1.0",
|
||||||
"@smithy/node-http-handler": "2.1.5",
|
"@smithy/node-http-handler": "2.1.5",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.87",
|
"@swc/core": "1.3.90",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "6.0.1",
|
"archiver": "6.0.1",
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
"jsonld": "8.3.1",
|
"jsonld": "8.3.1",
|
||||||
"jsrsasign": "10.8.6",
|
"jsrsasign": "10.8.6",
|
||||||
"megalodon": "workspace:*",
|
"megalodon": "workspace:*",
|
||||||
"meilisearch": "0.34.2",
|
"meilisearch": "0.35.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"microformats-parser": "1.5.2",
|
"microformats-parser": "1.5.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -158,7 +158,7 @@
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.21.8",
|
"systeminformation": "5.21.9",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
|
@ -191,34 +191,34 @@
|
||||||
"@types/jsdom": "21.1.3",
|
"@types/jsdom": "21.1.3",
|
||||||
"@types/jsonld": "1.5.10",
|
"@types/jsonld": "1.5.10",
|
||||||
"@types/jsrsasign": "10.5.9",
|
"@types/jsrsasign": "10.5.9",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.2",
|
||||||
"@types/ms": "0.7.31",
|
"@types/ms": "0.7.32",
|
||||||
"@types/node": "20.6.4",
|
"@types/node": "20.7.1",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.11",
|
"@types/nodemailer": "6.4.11",
|
||||||
"@types/oauth": "0.9.2",
|
"@types/oauth": "0.9.2",
|
||||||
"@types/oauth2orize": "1.11.1",
|
"@types/oauth2orize": "1.11.1",
|
||||||
"@types/oauth2orize-pkce": "0.1.0",
|
"@types/oauth2orize-pkce": "0.1.0",
|
||||||
"@types/pg": "8.10.2",
|
"@types/pg": "8.10.3",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.7",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/qrcode": "1.5.2",
|
"@types/qrcode": "1.5.2",
|
||||||
"@types/random-seed": "0.3.3",
|
"@types/random-seed": "0.3.3",
|
||||||
"@types/ratelimiter": "3.4.4",
|
"@types/ratelimiter": "3.4.4",
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.5",
|
||||||
"@types/sanitize-html": "2.9.0",
|
"@types/sanitize-html": "2.9.1",
|
||||||
"@types/semver": "7.5.2",
|
"@types/semver": "7.5.3",
|
||||||
"@types/sharp": "0.32.0",
|
"@types/sharp": "0.32.0",
|
||||||
"@types/simple-oauth2": "5.0.4",
|
"@types/simple-oauth2": "5.0.5",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.3",
|
||||||
"@types/tinycolor2": "1.4.4",
|
"@types/tinycolor2": "1.4.4",
|
||||||
"@types/tmp": "0.2.4",
|
"@types/tmp": "0.2.4",
|
||||||
"@types/uuid": "^9.0.4",
|
"@types/uuid": "^9.0.4",
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.1",
|
||||||
"@types/web-push": "3.6.0",
|
"@types/web-push": "3.6.1",
|
||||||
"@types/ws": "8.5.5",
|
"@types/ws": "8.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.3",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
|
|
|
@ -66,6 +66,7 @@ export async function masterMain() {
|
||||||
showNodejsVersion();
|
showNodejsVersion();
|
||||||
config = loadConfigBoot();
|
config = loadConfigBoot();
|
||||||
//await connectDb();
|
//await connectDb();
|
||||||
|
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bootLogger.error('Fatal error occurred during initialization', null, true);
|
bootLogger.error('Fatal error occurred during initialization', null, true);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -89,6 +89,7 @@ type Source = {
|
||||||
perChannelMaxNoteCacheCount?: number;
|
perChannelMaxNoteCacheCount?: number;
|
||||||
perUserNotificationsMaxCount?: number;
|
perUserNotificationsMaxCount?: number;
|
||||||
deactivateAntennaThreshold?: number;
|
deactivateAntennaThreshold?: number;
|
||||||
|
pidFile: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
|
@ -163,6 +164,7 @@ export type Config = {
|
||||||
perChannelMaxNoteCacheCount: number;
|
perChannelMaxNoteCacheCount: number;
|
||||||
perUserNotificationsMaxCount: number;
|
perUserNotificationsMaxCount: number;
|
||||||
deactivateAntennaThreshold: number;
|
deactivateAntennaThreshold: number;
|
||||||
|
pidFile: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
@ -255,6 +257,7 @@ export function loadConfig(): Config {
|
||||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
|
pidFile: config.pidFile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
|
||||||
* Maximum image description length that can be stored in DB.
|
* Maximum image description length that can be stored in DB.
|
||||||
* Surrogate pairs count as one
|
* Surrogate pairs count as one
|
||||||
*/
|
*/
|
||||||
export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
|
export const DB_MAX_IMAGE_COMMENT_LENGTH = 8192;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// ブラウザで直接表示することを許可するファイルの種類のリスト
|
// ブラウザで直接表示することを許可するファイルの種類のリスト
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.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';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -50,7 +50,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'antennaCreated':
|
case 'antennaCreated':
|
||||||
this.antennas.push({
|
this.antennas.push({
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -160,7 +160,7 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'userChangeSuspendedState':
|
case 'userChangeSuspendedState':
|
||||||
case 'remoteUserUpdated': {
|
case 'remoteUserUpdated': {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { query } from '@/misc/prelude/url.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';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
|
||||||
|
|
|
@ -423,6 +423,10 @@ export class DriveService {
|
||||||
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
|
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.backgroundId) {
|
||||||
|
q.andWhere('file.id != :backgroundId', { backgroundId: user.backgroundId });
|
||||||
|
}
|
||||||
|
|
||||||
//This selete is hard coded, be careful if change database schema
|
//This selete is hard coded, be careful if change database schema
|
||||||
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
|
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
|
||||||
q.orderBy('file.id', 'ASC');
|
q.orderBy('file.id', 'ASC');
|
||||||
|
|
|
@ -5,27 +5,254 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
|
||||||
import type { MiAntenna } from '@/models/Antenna.js';
|
import type { MiAntenna } from '@/models/Antenna.js';
|
||||||
import type {
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
StreamChannels,
|
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||||
AdminStreamTypes,
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
AntennaStreamTypes,
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
BroadcastTypes,
|
import type { MiSignin } from '@/models/Signin.js';
|
||||||
DriveStreamTypes,
|
import type { MiPage } from '@/models/Page.js';
|
||||||
InternalStreamTypes,
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
MainStreamTypes,
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
NoteStreamTypes,
|
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||||
UserListStreamTypes,
|
|
||||||
RoleTimelineStreamTypes,
|
|
||||||
} from '@/server/api/stream/types.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.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;
|
||||||
|
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 UserListEventTypes {
|
||||||
|
userAdded: Packed<'User'>;
|
||||||
|
userRemoved: Packed<'User'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaEventTypes {
|
||||||
|
note: MiNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>;
|
||||||
|
};
|
||||||
|
userList: {
|
||||||
|
name: `userListStream:${MiUserList['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||||
|
};
|
||||||
|
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()
|
@Injectable()
|
||||||
export class GlobalEventService {
|
export class GlobalEventService {
|
||||||
|
@ -51,7 +278,7 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,17 +288,17 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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, {
|
this.publish(`noteStream:${noteId}`, type, {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
body: value,
|
body: value,
|
||||||
|
@ -79,17 +306,17 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +326,7 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { bindThis } from '@/decorators.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';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -46,7 +46,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metaUpdated': {
|
case 'metaUpdated': {
|
||||||
this.cache = body;
|
this.cache = body;
|
||||||
|
|
|
@ -110,9 +110,8 @@ class NotificationManager {
|
||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||||
this.notificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
notifierId: this.notifier.id,
|
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
});
|
}, this.notifier.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -515,9 +514,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}).then(followings => {
|
}).then(followings => {
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In } from 'typeorm';
|
import { DataSource, In, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
|
@ -20,7 +20,7 @@ import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import { MiPoll, type IPoll } from '@/models/Poll.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
|
@ -105,9 +105,8 @@ class NotificationManager {
|
||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||||
this.notificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
notifierId: this.notifier.id,
|
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
});
|
}, this.notifier.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,6 +154,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -170,12 +172,12 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutedNotesRepository)
|
||||||
|
private mutedNotesRepository: MutedNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.noteThreadMutingsRepository)
|
@Inject(DI.noteThreadMutingsRepository)
|
||||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||||
|
|
||||||
|
@ -420,7 +422,31 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.poll != null) {
|
||||||
|
// Start transaction
|
||||||
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
|
await transactionalEntityManager.update(MiNote, oldnote.id, note);
|
||||||
|
|
||||||
|
const poll = new MiPoll({
|
||||||
|
noteId: note.id,
|
||||||
|
choices: data.poll!.choices,
|
||||||
|
expiresAt: data.poll!.expiresAt,
|
||||||
|
multiple: data.poll!.multiple,
|
||||||
|
votes: new Array(data.poll!.choices.length).fill(0),
|
||||||
|
noteVisibility: note.visibility,
|
||||||
|
userId: user.id,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!oldnote.hasPoll) {
|
||||||
|
await transactionalEntityManager.insert(MiPoll, poll);
|
||||||
|
} else {
|
||||||
|
await transactionalEntityManager.update(MiPoll, oldnote.id, poll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await this.notesRepository.update(oldnote.id, note);
|
await this.notesRepository.update(oldnote.id, note);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.channel) {
|
if (data.channel) {
|
||||||
this.redisClient.xadd(
|
this.redisClient.xadd(
|
||||||
|
@ -484,6 +510,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (data.poll && data.poll.expiresAt) {
|
if (data.poll && data.poll.expiresAt) {
|
||||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||||
|
this.queueService.endedPollNotificationQueue.remove(note.id);
|
||||||
this.queueService.endedPollNotificationQueue.add(note.id, {
|
this.queueService.endedPollNotificationQueue.add(note.id, {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, {
|
}, {
|
||||||
|
@ -522,10 +549,17 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// Pack the note
|
// Pack the note
|
||||||
const noteObj = await this.noteEntityService.pack(note);
|
const noteObj = await this.noteEntityService.pack(note);
|
||||||
|
if (data.poll != null) {
|
||||||
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
||||||
updatedAt: note.updatedAt!,
|
cw: note.cw,
|
||||||
|
text: note.text!,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
||||||
|
cw: note.cw,
|
||||||
|
text: note.text!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.roleService.addNoteToRoleTimeline(noteObj);
|
this.roleService.addNoteToRoleTimeline(noteObj);
|
||||||
|
|
||||||
|
@ -716,7 +750,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
|
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
|
||||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||||
: this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user);
|
: this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, false), user);
|
||||||
|
|
||||||
return this.apRendererService.addContext(content);
|
return this.apRendererService.addContext(content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { NotificationEntityService } from '@/core/entities/NotificationEntitySer
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
|
@ -38,6 +39,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private userListService: UserListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,27 +76,59 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
public async createNotification(
|
public async createNotification(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: MiNotification['type'],
|
type: MiNotification['type'],
|
||||||
data: Partial<MiNotification>,
|
data: Omit<Partial<MiNotification>, 'notifierId'>,
|
||||||
|
notifierId?: MiUser['id'] | null,
|
||||||
): Promise<MiNotification | null> {
|
): Promise<MiNotification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
const isMuted = profile.mutingNotificationTypes.includes(type);
|
|
||||||
if (isMuted) return null;
|
|
||||||
|
|
||||||
if (data.notifierId) {
|
// 古いMisskeyバージョンのキャッシュが残っている可能性がある
|
||||||
if (notifieeId === data.notifierId) {
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
const recieveConfig = (profile.notificationRecieveConfig ?? {})[type];
|
||||||
|
if (recieveConfig?.type === 'never') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifierId) {
|
||||||
|
if (notifieeId === notifierId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
if (mutings.has(data.notifierId)) {
|
if (mutings.has(notifierId)) {
|
||||||
return null;
|
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 = {
|
const notification = {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
type: type,
|
type: type,
|
||||||
|
notifierId: notifierId,
|
||||||
...data,
|
...data,
|
||||||
} as MiNotification;
|
} as MiNotification;
|
||||||
|
|
||||||
|
@ -117,8 +151,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
if (type === 'follow') this.emailNotificationFollow(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: data.notifierId! }));
|
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
|
|
|
@ -219,10 +219,9 @@ export class ReactionService {
|
||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
this.notificationService.createNotification(note.userId, 'reaction', {
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.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 { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
@ -32,6 +32,7 @@ export type RolePolicies = {
|
||||||
inviteExpirationTime: number;
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
|
canUseTranslator: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
driveCapacityMb: number;
|
driveCapacityMb: number;
|
||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
|
@ -56,6 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
inviteExpirationTime: 0,
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
|
canUseTranslator: true,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
|
@ -114,7 +116,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get();
|
||||||
|
@ -300,6 +302,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
|
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -230,8 +230,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
notifierId: followee.id,
|
}, followee.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
@ -304,8 +303,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'follow', {
|
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', {
|
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
notifierId: follower.id,
|
|
||||||
followRequestId: followRequest.id,
|
followRequestId: followRequest.id,
|
||||||
});
|
}, follower.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { UserListJoiningsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
|
@ -16,12 +17,22 @@ import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListService {
|
export class UserListService implements OnApplicationShutdown {
|
||||||
public static TooManyUsersError = class extends Error {};
|
public static TooManyUsersError = class extends Error {};
|
||||||
|
|
||||||
|
public membersCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListJoiningsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
|
@ -32,10 +43,48 @@ export class UserListService {
|
||||||
private proxyAccountService: ProxyAccountService,
|
private proxyAccountService: ProxyAccountService,
|
||||||
private queueService: QueueService,
|
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
|
@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({
|
const currentCount = await this.userListJoiningsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
|
@ -50,6 +99,7 @@ export class UserListService {
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as MiUserListJoining);
|
} as MiUserListJoining);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.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';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -45,7 +45,7 @@ export class WebhookService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'webhookCreated':
|
case 'webhookCreated':
|
||||||
if (body.active) {
|
if (body.active) {
|
||||||
|
|
|
@ -437,6 +437,7 @@ export class ApRendererService {
|
||||||
},
|
},
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
|
quoteUri: quote,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
|
@ -453,9 +454,10 @@ export class ApRendererService {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
const isSystem = user.username.includes('.');
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const [avatar, banner, profile] = await Promise.all([
|
const [avatar, banner, background, profile] = await Promise.all([
|
||||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
||||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
||||||
|
user.backgroundId ? this.driveFilesRepository.findOneBy({ id: user.backgroundId }) : undefined,
|
||||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -495,11 +497,13 @@ export class ApRendererService {
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
|
backgroundUrl: background ? this.renderImage(background) : null,
|
||||||
tag,
|
tag,
|
||||||
manuallyApprovesFollowers: user.isLocked,
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
discoverable: user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
speakAsCat: user.speakAsCat,
|
||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -519,6 +523,10 @@ export class ApRendererService {
|
||||||
person['vcard:Address'] = profile.location;
|
person['vcard:Address'] = profile.location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.listenbrainz) {
|
||||||
|
person.listenbrainz = profile.listenbrainz;
|
||||||
|
}
|
||||||
|
|
||||||
return person;
|
return person;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,6 +600,148 @@ export class ApRendererService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async renderUpNote(note: MiNote, dive = true): Promise<IPost> {
|
||||||
|
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||||
|
if (ids.length === 0) return [];
|
||||||
|
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||||
|
return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null);
|
||||||
|
};
|
||||||
|
|
||||||
|
let inReplyTo;
|
||||||
|
let inReplyToNote: MiNote | null;
|
||||||
|
|
||||||
|
if (note.replyId) {
|
||||||
|
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||||
|
|
||||||
|
if (inReplyToNote != null) {
|
||||||
|
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
|
||||||
|
|
||||||
|
if (inReplyToUserExist) {
|
||||||
|
if (inReplyToNote.uri) {
|
||||||
|
inReplyTo = inReplyToNote.uri;
|
||||||
|
} else {
|
||||||
|
if (dive) {
|
||||||
|
inReplyTo = await this.renderUpNote(inReplyToNote, false);
|
||||||
|
} else {
|
||||||
|
inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inReplyTo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote;
|
||||||
|
|
||||||
|
if (note.renoteId) {
|
||||||
|
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
|
||||||
|
|
||||||
|
if (renote) {
|
||||||
|
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||||
|
|
||||||
|
const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
|
||||||
|
|
||||||
|
let to: string[] = [];
|
||||||
|
let cc: string[] = [];
|
||||||
|
|
||||||
|
if (note.visibility === 'public') {
|
||||||
|
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
|
cc = [`${attributedTo}/followers`].concat(mentions);
|
||||||
|
} else if (note.visibility === 'home') {
|
||||||
|
to = [`${attributedTo}/followers`];
|
||||||
|
cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
|
||||||
|
} else if (note.visibility === 'followers') {
|
||||||
|
to = [`${attributedTo}/followers`];
|
||||||
|
cc = mentions;
|
||||||
|
} else {
|
||||||
|
to = mentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentionedUsers = note.mentions && note.mentions.length > 0 ? await this.usersRepository.findBy({
|
||||||
|
id: In(note.mentions),
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
|
||||||
|
const mentionTags = mentionedUsers.map(u => this.renderMention(u as MiLocalUser | MiRemoteUser));
|
||||||
|
|
||||||
|
const files = await getPromisedFiles(note.fileIds);
|
||||||
|
|
||||||
|
const text = note.text ?? '';
|
||||||
|
let poll: MiPoll | null = null;
|
||||||
|
|
||||||
|
if (note.hasPoll) {
|
||||||
|
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
let apText = text;
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
apText += `\n\nRE: ${quote}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||||
|
|
||||||
|
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||||
|
text: apText,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const emojis = await this.getEmojis(note.emojis);
|
||||||
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
|
const tag = [
|
||||||
|
...hashtagTags,
|
||||||
|
...mentionTags,
|
||||||
|
...apemojis,
|
||||||
|
];
|
||||||
|
|
||||||
|
const asPoll = poll ? {
|
||||||
|
type: 'Question',
|
||||||
|
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||||
|
text: text,
|
||||||
|
})),
|
||||||
|
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
||||||
|
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||||
|
type: 'Note',
|
||||||
|
name: text,
|
||||||
|
replies: {
|
||||||
|
type: 'Collection',
|
||||||
|
totalItems: poll!.votes[i],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
} as const : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${this.config.url}/notes/${note.id}`,
|
||||||
|
type: 'Note',
|
||||||
|
attributedTo,
|
||||||
|
summary: summary ?? undefined,
|
||||||
|
content: content ?? undefined,
|
||||||
|
updated: note.updatedAt?.toISOString(),
|
||||||
|
_misskey_content: text,
|
||||||
|
source: {
|
||||||
|
content: text,
|
||||||
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
},
|
||||||
|
_misskey_quote: quote,
|
||||||
|
quoteUrl: quote,
|
||||||
|
quoteUri: quote,
|
||||||
|
published: note.createdAt.toISOString(),
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
inReplyTo,
|
||||||
|
attachment: files.map(x => this.renderDocument(x)),
|
||||||
|
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
||||||
|
tag,
|
||||||
|
...asPoll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
||||||
return {
|
return {
|
||||||
|
@ -627,6 +777,8 @@ export class ApRendererService {
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
Hashtag: 'as:Hashtag',
|
Hashtag: 'as:Hashtag',
|
||||||
quoteUrl: 'as:quoteUrl',
|
quoteUrl: 'as:quoteUrl',
|
||||||
|
fedibird: 'http://fedibird.com/ns#',
|
||||||
|
quoteUri: 'fedibird:quoteUri',
|
||||||
// Mastodon
|
// Mastodon
|
||||||
toot: 'http://joinmastodon.org/ns#',
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
Emoji: 'toot:Emoji',
|
Emoji: 'toot:Emoji',
|
||||||
|
@ -643,6 +795,13 @@ export class ApRendererService {
|
||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
|
// Firefish
|
||||||
|
firefish: "https://joinfirefish.org/ns#",
|
||||||
|
speakAsCat: "firefish:speakAsCat",
|
||||||
|
// Sharkey
|
||||||
|
sharkey: "https://joinsharkey.org/ns#",
|
||||||
|
backgroundUrl: "sharkey:backgroundUrl",
|
||||||
|
listenbrainz: "sharkey:listenbrainz",
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
},
|
},
|
||||||
|
|
|
@ -205,12 +205,12 @@ export class ApNoteService {
|
||||||
// 引用
|
// 引用
|
||||||
let quote: MiNote | undefined | null = null;
|
let quote: MiNote | undefined | null = null;
|
||||||
|
|
||||||
if (note._misskey_quote ?? note.quoteUrl) {
|
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||||
const tryResolveNote = async (uri: string): Promise<
|
const tryResolveNote = async (uri: string): Promise<
|
||||||
| { status: 'ok'; res: MiNote }
|
| { status: 'ok'; res: MiNote }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
> => {
|
> => {
|
||||||
if (!/^https?:/.test(uri)) return { status: 'permerror' };
|
if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri);
|
const res = await this.resolveNote(uri);
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res == null) return { status: 'permerror' };
|
||||||
|
@ -222,7 +222,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||||
const results = await Promise.all(uris.map(tryResolveNote));
|
const results = await Promise.all(uris.map(tryResolveNote));
|
||||||
|
|
||||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||||
|
@ -413,7 +413,7 @@ export class ApNoteService {
|
||||||
// 引用
|
// 引用
|
||||||
let quote: MiNote | undefined | null = null;
|
let quote: MiNote | undefined | null = null;
|
||||||
|
|
||||||
if (note._misskey_quote ?? note.quoteUrl) {
|
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||||
const tryResolveNote = async (uri: string): Promise<
|
const tryResolveNote = async (uri: string): Promise<
|
||||||
| { status: 'ok'; res: MiNote }
|
| { status: 'ok'; res: MiNote }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
|
@ -430,7 +430,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||||
const results = await Promise.all(uris.map(tryResolveNote));
|
const results = await Promise.all(uris.map(tryResolveNote));
|
||||||
|
|
||||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||||
|
|
|
@ -225,8 +225,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
|
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
|
||||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
||||||
if (img == null) return null;
|
if (img == null) return null;
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
|
@ -235,10 +235,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return {
|
return {
|
||||||
avatarId: avatar?.id ?? null,
|
avatarId: avatar?.id ?? null,
|
||||||
bannerId: banner?.id ?? null,
|
bannerId: banner?.id ?? null,
|
||||||
|
backgroundId: background?.id ?? null,
|
||||||
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||||
|
backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
|
||||||
avatarBlurhash: avatar?.blurhash ?? null,
|
avatarBlurhash: avatar?.blurhash ?? null,
|
||||||
bannerBlurhash: banner?.blurhash ?? null,
|
bannerBlurhash: banner?.blurhash ?? null,
|
||||||
|
backgroundBlurhash: background?.blurhash ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +307,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
avatarId: null,
|
avatarId: null,
|
||||||
bannerId: null,
|
bannerId: null,
|
||||||
|
backgroundId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
|
@ -326,6 +330,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
tags,
|
tags,
|
||||||
isBot,
|
isBot,
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
|
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
|
||||||
emojis,
|
emojis,
|
||||||
})) as MiRemoteUser;
|
})) as MiRemoteUser;
|
||||||
|
|
||||||
|
@ -337,6 +342,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
userHost: host,
|
userHost: host,
|
||||||
|
listenbrainz: person.listenbrainz ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (person.publicKey) {
|
if (person.publicKey) {
|
||||||
|
@ -382,7 +388,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
//#region アバターとヘッダー画像をフェッチ
|
//#region アバターとヘッダー画像をフェッチ
|
||||||
try {
|
try {
|
||||||
const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image);
|
const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image, person.backgroundUrl);
|
||||||
await this.usersRepository.update(user.id, updates);
|
await this.usersRepository.update(user.id, updates);
|
||||||
user = { ...user, ...updates };
|
user = { ...user, ...updates };
|
||||||
|
|
||||||
|
@ -460,12 +466,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
tags,
|
tags,
|
||||||
isBot: getApType(object) === 'Service',
|
isBot: getApType(object) === 'Service',
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
|
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
|
||||||
isLocked: person.manuallyApprovesFollowers,
|
isLocked: person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
|
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(() => ({}))),
|
||||||
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
const moving = ((): boolean => {
|
const moving = ((): boolean => {
|
||||||
// 移行先がない→ある
|
// 移行先がない→ある
|
||||||
|
@ -503,6 +510,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
|
listenbrainz: person.listenbrainz ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id });
|
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id });
|
||||||
|
|
|
@ -118,6 +118,8 @@ export interface IPost extends IObject {
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
_misskey_content?: string;
|
_misskey_content?: string;
|
||||||
quoteUrl?: string;
|
quoteUrl?: string;
|
||||||
|
quoteUri?: string;
|
||||||
|
updated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuestion extends IObject {
|
export interface IQuestion extends IObject {
|
||||||
|
@ -129,6 +131,7 @@ export interface IQuestion extends IObject {
|
||||||
};
|
};
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
quoteUrl?: string;
|
quoteUrl?: string;
|
||||||
|
quoteUri?: string;
|
||||||
oneOf?: IQuestionChoice[];
|
oneOf?: IQuestionChoice[];
|
||||||
anyOf?: IQuestionChoice[];
|
anyOf?: IQuestionChoice[];
|
||||||
endTime?: Date;
|
endTime?: Date;
|
||||||
|
@ -180,6 +183,8 @@ export interface IActor extends IObject {
|
||||||
};
|
};
|
||||||
'vcard:bday'?: string;
|
'vcard:bday'?: string;
|
||||||
'vcard:Address'?: string;
|
'vcard:Address'?: string;
|
||||||
|
listenbrainz?: string;
|
||||||
|
backgroundUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCollection = (object: IObject): object is ICollection =>
|
export const isCollection = (object: IObject): object is ICollection =>
|
||||||
|
|
|
@ -312,6 +312,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
const packed: Packed<'Note'> = await awaitAll({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: note.createdAt.toISOString(),
|
createdAt: note.createdAt.toISOString(),
|
||||||
|
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
|
@ -342,7 +343,6 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
url: note.url ?? undefined,
|
url: note.url ?? undefined,
|
||||||
updatedAt: note.updatedAt != null ? note.updatedAt.toISOString() : undefined,
|
|
||||||
...(meId ? {
|
...(meId ? {
|
||||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
@ -364,7 +364,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
} : {}),
|
} : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (packed.user.isCat && packed.text) {
|
if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
|
||||||
const tokens = packed.text ? mfm.parse(packed.text) : [];
|
const tokens = packed.text ? mfm.parse(packed.text) : [];
|
||||||
function nyaizeNode(node: mfm.MfmNode) {
|
function nyaizeNode(node: mfm.MfmNode) {
|
||||||
if (node.type === 'quote') return;
|
if (node.type === 'quote') return;
|
||||||
|
|
|
@ -308,6 +308,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
bannerBlurhash: banner.blurhash,
|
bannerBlurhash: banner.blurhash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (user.backgroundId != null && user.backgroundUrl === null) {
|
||||||
|
const background = await this.driveFilesRepository.findOneByOrFail({ id: user.backgroundId });
|
||||||
|
user.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: background.blurhash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
|
@ -352,6 +360,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
isBot: user.isBot ?? falsy,
|
isBot: user.isBot ?? falsy,
|
||||||
isCat: user.isCat ?? falsy,
|
isCat: user.isCat ?? falsy,
|
||||||
|
speakAsCat: user.speakAsCat ?? falsy,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
|
@ -384,6 +393,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: user.backgroundBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
isSuspended: user.isSuspended ?? falsy,
|
isSuspended: user.isSuspended ?? falsy,
|
||||||
|
@ -428,6 +439,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(opts.detail && isMe ? {
|
...(opts.detail && isMe ? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
backgroundId: user.backgroundId,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||||
|
@ -458,7 +470,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
|
|
|
@ -67,7 +67,8 @@ export class MiDriveFile {
|
||||||
public size: number;
|
public size: number;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512, nullable: true,
|
length: 8192,
|
||||||
|
nullable: true,
|
||||||
comment: 'The comment of the DriveFile.',
|
comment: 'The comment of the DriveFile.',
|
||||||
})
|
})
|
||||||
public comment: string | null;
|
public comment: string | null;
|
||||||
|
|
|
@ -24,6 +24,12 @@ export class MiNote {
|
||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The update time of the Note.',
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
public updatedAt: Date | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
@ -239,12 +245,6 @@ export class MiNote {
|
||||||
comment: '[Denormalized]',
|
comment: '[Denormalized]',
|
||||||
})
|
})
|
||||||
public renoteUserHost: string | null;
|
public renoteUserHost: string | null;
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column('timestamp with time zone', {
|
|
||||||
comment: 'The update time of the Note.',
|
|
||||||
})
|
|
||||||
public updatedAt: Date | null;
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
constructor(data: Partial<MiNote>) {
|
constructor(data: Partial<MiNote>) {
|
||||||
|
|
|
@ -124,6 +124,19 @@ export class MiUser {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public banner: MiDriveFile | null;
|
public banner: MiDriveFile | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
comment: 'The ID of background DriveFile.',
|
||||||
|
})
|
||||||
|
public backgroundId: MiDriveFile['id'] | null;
|
||||||
|
|
||||||
|
@OneToOne(type => MiDriveFile, {
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public background: MiDriveFile | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512, nullable: true,
|
length: 512, nullable: true,
|
||||||
})
|
})
|
||||||
|
@ -134,6 +147,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public bannerUrl: string | null;
|
public bannerUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 512, nullable: true,
|
||||||
|
})
|
||||||
|
public backgroundUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, nullable: true,
|
length: 128, nullable: true,
|
||||||
})
|
})
|
||||||
|
@ -144,6 +162,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public bannerBlurhash: string | null;
|
public bannerBlurhash: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
})
|
||||||
|
public backgroundBlurhash: string | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, array: true, default: '{}',
|
length: 128, array: true, default: '{}',
|
||||||
|
@ -174,6 +197,12 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public isCat: boolean;
|
public isCat: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
comment: 'Whether the User speaks in nya.',
|
||||||
|
})
|
||||||
|
public speakAsCat: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is the root.',
|
comment: 'Whether the User is the root.',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/ty
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiPage } from './Page.js';
|
import { MiPage } from './Page.js';
|
||||||
|
import { MiUserList } from './UserList.js';
|
||||||
|
|
||||||
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
||||||
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
||||||
|
@ -229,16 +230,25 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public mutedInstances: string[];
|
public mutedInstances: string[];
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('jsonb', {
|
||||||
enum: [
|
default: {},
|
||||||
...notificationTypes,
|
|
||||||
// マイグレーションで削除が困難なので古いenumは残しておく
|
|
||||||
...obsoleteNotificationTypes,
|
|
||||||
],
|
|
||||||
array: true,
|
|
||||||
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', {
|
@Column('varchar', {
|
||||||
length: 32, array: true, default: '{}',
|
length: 32, array: true, default: '{}',
|
||||||
|
|
|
@ -17,12 +17,12 @@ export const packedNoteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
deletedAt: {
|
updatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
updatedAt: {
|
deletedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
|
@ -147,7 +147,7 @@ export const packedNoteSchema = {
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,6 +55,10 @@ export const packedUserLiteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
speakAsCat: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
onlineStatus: {
|
onlineStatus: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'url',
|
format: 'url',
|
||||||
|
@ -118,6 +122,15 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
|
backgroundUrl: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'url',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
|
backgroundBlurhash: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
isLocked: {
|
isLocked: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -300,6 +313,11 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
backgroundId: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
injectFeaturedNote: {
|
injectFeaturedNote: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
@ -393,14 +411,10 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutingNotificationTypes: {
|
notificationRecieveConfig: {
|
||||||
type: 'array',
|
type: 'object',
|
||||||
nullable: true, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
emailNotificationTypes: {
|
emailNotificationTypes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class ImportUserListsProcessorService {
|
||||||
|
|
||||||
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
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) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error in line:${linenum} ${e}`);
|
this.logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.adsRepository.insert({
|
const ad = await this.adsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
expiresAt: new Date(ps.expiresAt),
|
expiresAt: new Date(ps.expiresAt),
|
||||||
|
@ -53,7 +55,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
ratio: ps.ratio,
|
ratio: ps.ratio,
|
||||||
place: ps.place,
|
place: ps.place,
|
||||||
memo: ps.memo,
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
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);
|
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||||
|
|
||||||
await this.adsRepository.delete(ad.id);
|
await this.adsRepository.delete(ad.id);
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'deleteAd', {
|
||||||
|
adId: ad.id,
|
||||||
|
ad: ad,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
publishing: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -36,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
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();
|
const ads = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
return ads;
|
return ads;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
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),
|
startsAt: new Date(ps.startsAt),
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'updateAd', {
|
||||||
|
adId: ad.id,
|
||||||
|
before: ad,
|
||||||
|
after: updatedAd,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||||
mutedWords: profile.mutedWords,
|
mutedWords: profile.mutedWords,
|
||||||
mutedInstances: profile.mutedInstances,
|
mutedInstances: profile.mutedInstances,
|
||||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
notificationRecieveConfig: profile.notificationRecieveConfig,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isSilenced: isSilenced,
|
isSilenced: isSilenced,
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -65,7 +66,7 @@ export const paramDef = {
|
||||||
folderId: { type: 'string', format: 'misskey:id', nullable: true },
|
folderId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
name: { type: 'string' },
|
name: { type: 'string' },
|
||||||
isSensitive: { type: 'boolean' },
|
isSensitive: { type: 'boolean' },
|
||||||
comment: { type: 'string', nullable: true, maxLength: 512 },
|
comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH },
|
||||||
},
|
},
|
||||||
required: ['fileId'],
|
required: ['fileId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -33,7 +34,7 @@ export const paramDef = {
|
||||||
url: { type: 'string' },
|
url: { type: 'string' },
|
||||||
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||||
isSensitive: { type: 'boolean', default: false },
|
isSensitive: { type: 'boolean', default: false },
|
||||||
comment: { type: 'string', nullable: true, maxLength: 512, default: null },
|
comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null },
|
||||||
marker: { type: 'string', nullable: true, default: null },
|
marker: { type: 'string', nullable: true, default: null },
|
||||||
force: { type: 'boolean', default: false },
|
force: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,6 +60,12 @@ export const meta = {
|
||||||
id: '0d8f5629-f210-41c2-9433-735831a58595',
|
id: '0d8f5629-f210-41c2-9433-735831a58595',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noSuchBackground: {
|
||||||
|
message: 'No such background file.',
|
||||||
|
code: 'NO_SUCH_BACKGROUND',
|
||||||
|
id: '0d8f5629-f210-41c2-9433-735831a58582',
|
||||||
|
},
|
||||||
|
|
||||||
avatarNotAnImage: {
|
avatarNotAnImage: {
|
||||||
message: 'The file specified as an avatar is not an image.',
|
message: 'The file specified as an avatar is not an image.',
|
||||||
code: 'AVATAR_NOT_AN_IMAGE',
|
code: 'AVATAR_NOT_AN_IMAGE',
|
||||||
|
@ -72,6 +78,12 @@ export const meta = {
|
||||||
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
|
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
backgroundNotAnImage: {
|
||||||
|
message: 'The file specified as a background is not an image.',
|
||||||
|
code: 'BACKGROUND_NOT_AN_IMAGE',
|
||||||
|
id: '75aedb19-2afd-4e6d-87fc-67941256fa40',
|
||||||
|
},
|
||||||
|
|
||||||
noSuchPage: {
|
noSuchPage: {
|
||||||
message: 'No such page.',
|
message: 'No such page.',
|
||||||
code: 'NO_SUCH_PAGE',
|
code: 'NO_SUCH_PAGE',
|
||||||
|
@ -133,6 +145,7 @@ export const paramDef = {
|
||||||
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
||||||
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
|
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
fields: {
|
fields: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
minItems: 0,
|
minItems: 0,
|
||||||
|
@ -156,6 +169,7 @@ export const paramDef = {
|
||||||
preventAiLearning: { type: 'boolean' },
|
preventAiLearning: { type: 'boolean' },
|
||||||
isBot: { type: 'boolean' },
|
isBot: { type: 'boolean' },
|
||||||
isCat: { type: 'boolean' },
|
isCat: { type: 'boolean' },
|
||||||
|
speakAsCat: { type: 'boolean' },
|
||||||
injectFeaturedNote: { type: 'boolean' },
|
injectFeaturedNote: { type: 'boolean' },
|
||||||
receiveAnnouncementEmail: { type: 'boolean' },
|
receiveAnnouncementEmail: { type: 'boolean' },
|
||||||
alwaysMarkNsfw: { type: 'boolean' },
|
alwaysMarkNsfw: { type: 'boolean' },
|
||||||
|
@ -166,9 +180,7 @@ export const paramDef = {
|
||||||
mutedInstances: { type: 'array', items: {
|
mutedInstances: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
mutingNotificationTypes: { type: 'array', items: {
|
notificationRecieveConfig: { type: 'object' },
|
||||||
type: 'string', enum: notificationTypes,
|
|
||||||
} },
|
|
||||||
emailNotificationTypes: { type: 'array', items: {
|
emailNotificationTypes: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -250,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
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.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
|
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
|
||||||
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
||||||
|
@ -261,6 +273,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||||
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
||||||
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||||
|
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
|
@ -300,6 +313,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
updates.bannerBlurhash = null;
|
updates.bannerBlurhash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.backgroundId) {
|
||||||
|
const background = await this.driveFilesRepository.findOneBy({ id: ps.backgroundId });
|
||||||
|
|
||||||
|
if (background == null || background.userId !== user.id) throw new ApiError(meta.errors.noSuchBackground);
|
||||||
|
if (!background.type.startsWith('image/')) throw new ApiError(meta.errors.backgroundNotAnImage);
|
||||||
|
|
||||||
|
updates.backgroundId = background.id;
|
||||||
|
updates.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
|
||||||
|
updates.backgroundBlurhash = background.blurhash;
|
||||||
|
} else if (ps.backgroundId === null) {
|
||||||
|
updates.backgroundId = null;
|
||||||
|
updates.backgroundUrl = null;
|
||||||
|
updates.backgroundBlurhash = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.pinnedPageId) {
|
if (ps.pinnedPageId) {
|
||||||
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
|
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await get(note.replyId);
|
await get(note.replyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.hasPoll) {
|
||||||
|
return await this.noteEntityService.packMany(conversation, me, { detail: true });
|
||||||
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(conversation, me);
|
return await this.noteEntityService.packMany(conversation, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,11 @@ describe('api:notes/create', () => {
|
||||||
.toBe(VALID);
|
.toBe(VALID);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('null post', () => {
|
// TODO
|
||||||
expect(v({ text: null }))
|
//test('null post', () => {
|
||||||
.toBe(INVALID);
|
// expect(v({ text: null }))
|
||||||
});
|
// .toBe(INVALID);
|
||||||
|
//});
|
||||||
|
|
||||||
test('0 characters post', () => {
|
test('0 characters post', () => {
|
||||||
expect(v({ text: '' }))
|
expect(v({ text: '' }))
|
||||||
|
|
|
@ -118,7 +118,7 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||||
nullable: false,
|
nullable: true,
|
||||||
},
|
},
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
|
@ -10,12 +10,13 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -23,6 +24,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
unavailable: {
|
||||||
|
message: 'Translate of notes unavailable.',
|
||||||
|
code: 'UNAVAILABLE',
|
||||||
|
id: '50a70314-2d8a-431b-b433-efa5cc56444c',
|
||||||
|
},
|
||||||
noSuchNote: {
|
noSuchNote: {
|
||||||
message: 'No such note.',
|
message: 'No such note.',
|
||||||
code: 'NO_SUCH_NOTE',
|
code: 'NO_SUCH_NOTE',
|
||||||
|
@ -47,14 +53,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const policies = await this.roleService.getUserPolicies(me.id);
|
||||||
|
if (!policies.canUseTranslator) {
|
||||||
|
throw new ApiError(meta.errors.unavailable);
|
||||||
|
}
|
||||||
|
|
||||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
||||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(await this.noteEntityService.isVisibleForMe(note, me ? me.id : null))) {
|
if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) {
|
||||||
return 204; // TODO: 良い感じのエラー返す
|
return 204; // TODO: 良い感じのエラー返す
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userListService.push(currentUser, userList, me);
|
await this.userListService.addMember(currentUser, userList, me);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof UserListService.TooManyUsersError) {
|
if (err instanceof UserListService.TooManyUsersError) {
|
||||||
throw new ApiError(meta.errors.tooManyUsers);
|
throw new ApiError(meta.errors.tooManyUsers);
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -53,12 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
private userListService: UserListService,
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Fetch the list
|
// Fetch the list
|
||||||
|
@ -77,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pull the user
|
await this.userListService.removeMember(user, userList);
|
||||||
await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id });
|
|
||||||
|
|
||||||
this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userListService.push(user, userList, me);
|
await this.userListService.addMember(user, userList, me);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof UserListService.TooManyUsersError) {
|
if (err instanceof UserListService.TooManyUsersError) {
|
||||||
throw new ApiError(meta.errors.tooManyUsers);
|
throw new ApiError(meta.errors.tooManyUsers);
|
||||||
|
|
|
@ -12,10 +12,10 @@ import type { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiUserProfile } from '@/models/_.js';
|
import { MiUserProfile } from '@/models/_.js';
|
||||||
|
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { ChannelsService } from './ChannelsService.js';
|
import type { ChannelsService } from './ChannelsService.js';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
import type Channel from './channel.js';
|
import type Channel from './channel.js';
|
||||||
import type { StreamEventEmitter, StreamMessages } from './types.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main stream connection
|
* Main stream connection
|
||||||
|
@ -122,7 +122,7 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
|
private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) {
|
||||||
this.sendMessageToWs(data.type, data.body);
|
this.sendMessageToWs(data.type, data.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
|
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
|
||||||
this.sendMessageToWs('noteUpdated', {
|
this.sendMessageToWs('noteUpdated', {
|
||||||
id: data.body.id,
|
id: data.body.id,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import type { StreamMessages } from '../types.js';
|
|
||||||
|
|
||||||
class AntennaChannel extends Channel {
|
class AntennaChannel extends Channel {
|
||||||
public readonly chName = 'antenna';
|
public readonly chName = 'antenna';
|
||||||
|
@ -35,7 +35,7 @@ class AntennaChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onEvent(data: StreamMessages['antenna']['payload']) {
|
private async onEvent(data: GlobalEvents['antenna']['payload']) {
|
||||||
if (data.type === 'note') {
|
if (data.type === 'note') {
|
||||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { StreamMessages } from '../types.js';
|
|
||||||
|
|
||||||
class RoleTimelineChannel extends Channel {
|
class RoleTimelineChannel extends Channel {
|
||||||
public readonly chName = 'roleTimeline';
|
public readonly chName = 'roleTimeline';
|
||||||
|
@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
|
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
|
||||||
if (data.type === 'note') {
|
if (data.type === 'note') {
|
||||||
const note = data.body;
|
const note = data.body;
|
||||||
|
|
||||||
|
|
|
@ -1,258 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey 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 { 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;
|
|
||||||
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'];
|
|
||||||
};
|
|
||||||
updated: {
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
type NoteStreamEventTypes = {
|
|
||||||
[key in keyof NoteStreamTypes]: {
|
|
||||||
id: MiNote['id'];
|
|
||||||
body: NoteStreamTypes[key];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface UserListStreamTypes {
|
|
||||||
userAdded: Packed<'User'>;
|
|
||||||
userRemoved: Packed<'User'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AntennaStreamTypes {
|
|
||||||
note: MiNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>>;
|
|
||||||
};
|
|
||||||
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>>;
|
|
||||||
};
|
|
||||||
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'];
|
|
|
@ -188,7 +188,7 @@ export class ClientServerService {
|
||||||
// Authenticate
|
// Authenticate
|
||||||
fastify.addHook('onRequest', async (request, reply) => {
|
fastify.addHook('onRequest', async (request, reply) => {
|
||||||
// %71ueueとかでリクエストされたら困るため
|
// %71ueueとかでリクエストされたら困るため
|
||||||
const url = decodeURI(request.url);
|
const url = decodeURI(request.routeOptions.url);
|
||||||
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
|
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
|
||||||
const token = request.cookies.token;
|
const token = request.cookies.token;
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
@ -728,8 +728,8 @@ export class ClientServerService {
|
||||||
|
|
||||||
fastify.setErrorHandler(async (error, request, reply) => {
|
fastify.setErrorHandler(async (error, request, reply) => {
|
||||||
const errId = randomUUID();
|
const errId = randomUUID();
|
||||||
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routerPath}: ${error.message}`, {
|
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, {
|
||||||
path: request.routerPath,
|
path: request.routeOptions.url,
|
||||||
params: request.params,
|
params: request.params,
|
||||||
query: request.query,
|
query: request.query,
|
||||||
code: error.name,
|
code: error.name,
|
||||||
|
|
|
@ -57,6 +57,9 @@ export const moderationLogTypes = [
|
||||||
'unmarkSensitiveDriveFile',
|
'unmarkSensitiveDriveFile',
|
||||||
'resolveAbuseReport',
|
'resolveAbuseReport',
|
||||||
'createInvitation',
|
'createInvitation',
|
||||||
|
'createAd',
|
||||||
|
'updateAd',
|
||||||
|
'deleteAd',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -202,4 +205,28 @@ export type ModerationLogPayloads = {
|
||||||
createInvitation: {
|
createInvitation: {
|
||||||
invitations: any[];
|
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];
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe('ユーザー', () => {
|
||||||
avatarBlurhash: user.avatarBlurhash,
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
speakAsCat: user.speakAsCat,
|
||||||
instance: user.instance,
|
instance: user.instance,
|
||||||
emojis: user.emojis,
|
emojis: user.emojis,
|
||||||
onlineStatus: user.onlineStatus,
|
onlineStatus: user.onlineStatus,
|
||||||
|
@ -94,6 +95,8 @@ describe('ユーザー', () => {
|
||||||
lastFetchedAt: user.lastFetchedAt,
|
lastFetchedAt: user.lastFetchedAt,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: user.backgroundBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: user.isSilenced,
|
isSilenced: user.isSilenced,
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
|
@ -167,6 +170,7 @@ describe('ユーザー', () => {
|
||||||
mutedWords: user.mutedWords,
|
mutedWords: user.mutedWords,
|
||||||
mutedInstances: user.mutedInstances,
|
mutedInstances: user.mutedInstances,
|
||||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
mutingNotificationTypes: user.mutingNotificationTypes,
|
||||||
|
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||||
emailNotificationTypes: user.emailNotificationTypes,
|
emailNotificationTypes: user.emailNotificationTypes,
|
||||||
achievements: user.achievements,
|
achievements: user.achievements,
|
||||||
loggedInDays: user.loggedInDays,
|
loggedInDays: user.loggedInDays,
|
||||||
|
@ -349,6 +353,7 @@ describe('ユーザー', () => {
|
||||||
assert.strictEqual(response.avatarBlurhash, null);
|
assert.strictEqual(response.avatarBlurhash, null);
|
||||||
assert.strictEqual(response.isBot, false);
|
assert.strictEqual(response.isBot, false);
|
||||||
assert.strictEqual(response.isCat, false);
|
assert.strictEqual(response.isCat, false);
|
||||||
|
assert.strictEqual(response.speakAsCat, false);
|
||||||
assert.strictEqual(response.instance, undefined);
|
assert.strictEqual(response.instance, undefined);
|
||||||
assert.deepStrictEqual(response.emojis, {});
|
assert.deepStrictEqual(response.emojis, {});
|
||||||
assert.strictEqual(response.onlineStatus, 'unknown');
|
assert.strictEqual(response.onlineStatus, 'unknown');
|
||||||
|
@ -363,6 +368,8 @@ describe('ユーザー', () => {
|
||||||
assert.strictEqual(response.lastFetchedAt, null);
|
assert.strictEqual(response.lastFetchedAt, null);
|
||||||
assert.strictEqual(response.bannerUrl, null);
|
assert.strictEqual(response.bannerUrl, null);
|
||||||
assert.strictEqual(response.bannerBlurhash, null);
|
assert.strictEqual(response.bannerBlurhash, null);
|
||||||
|
assert.strictEqual(response.backgroundUrl, null);
|
||||||
|
assert.strictEqual(response.backgroundBlurhash, null);
|
||||||
assert.strictEqual(response.isLocked, false);
|
assert.strictEqual(response.isLocked, false);
|
||||||
assert.strictEqual(response.isSilenced, false);
|
assert.strictEqual(response.isSilenced, false);
|
||||||
assert.strictEqual(response.isSuspended, false);
|
assert.strictEqual(response.isSuspended, false);
|
||||||
|
@ -415,6 +422,7 @@ describe('ユーザー', () => {
|
||||||
assert.deepStrictEqual(response.mutedWords, []);
|
assert.deepStrictEqual(response.mutedWords, []);
|
||||||
assert.deepStrictEqual(response.mutedInstances, []);
|
assert.deepStrictEqual(response.mutedInstances, []);
|
||||||
assert.deepStrictEqual(response.mutingNotificationTypes, []);
|
assert.deepStrictEqual(response.mutingNotificationTypes, []);
|
||||||
|
assert.deepStrictEqual(response.notificationRecieveConfig, {});
|
||||||
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
|
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
|
||||||
assert.deepStrictEqual(response.achievements, []);
|
assert.deepStrictEqual(response.achievements, []);
|
||||||
assert.deepStrictEqual(response.loggedInDays, 0);
|
assert.deepStrictEqual(response.loggedInDays, 0);
|
||||||
|
@ -479,6 +487,8 @@ describe('ユーザー', () => {
|
||||||
{ parameters: (): object => ({ isBot: false }) },
|
{ parameters: (): object => ({ isBot: false }) },
|
||||||
{ parameters: (): object => ({ isCat: true }) },
|
{ parameters: (): object => ({ isCat: true }) },
|
||||||
{ parameters: (): object => ({ isCat: false }) },
|
{ parameters: (): object => ({ isCat: false }) },
|
||||||
|
{ parameters: (): object => ({ speakAsCat: true }) },
|
||||||
|
{ parameters: (): object => ({ speakAsCat: false }) },
|
||||||
{ parameters: (): object => ({ injectFeaturedNote: true }) },
|
{ parameters: (): object => ({ injectFeaturedNote: true }) },
|
||||||
{ parameters: (): object => ({ injectFeaturedNote: false }) },
|
{ parameters: (): object => ({ injectFeaturedNote: false }) },
|
||||||
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
|
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
|
||||||
|
@ -495,8 +505,8 @@ describe('ユーザー', () => {
|
||||||
{ parameters: (): object => ({ mutedWords: [] }) },
|
{ parameters: (): object => ({ mutedWords: [] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: [] }) },
|
{ parameters: (): object => ({ mutedInstances: [] }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
|
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
|
{ parameters: (): object => ({ notificationRecieveConfig: {} }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
||||||
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
|
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
|
||||||
|
@ -555,6 +565,31 @@ describe('ユーザー', () => {
|
||||||
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('を書き換えることができる(Background)', async () => {
|
||||||
|
const aliceFile = (await uploadFile(alice)).body;
|
||||||
|
const parameters = { bannerId: aliceFile.id };
|
||||||
|
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
|
||||||
|
assert.match(response.backgroundUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
|
||||||
|
assert.match(response.backgroundBlurhash ?? '.', /[ -~]{54}/);
|
||||||
|
const expected = {
|
||||||
|
...meDetailed(alice, true),
|
||||||
|
backgroundId: aliceFile.id,
|
||||||
|
backgroundBlurhash: response.baackgroundBlurhash,
|
||||||
|
backgroundUrl: response.backgroundUrl,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(response, expected, inspect(parameters));
|
||||||
|
|
||||||
|
const parameters2 = { backgroundId: null };
|
||||||
|
const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
|
||||||
|
const expected2 = {
|
||||||
|
...meDetailed(alice, true),
|
||||||
|
backgroundId: null,
|
||||||
|
backgroundBlurhash: null,
|
||||||
|
backgroundUrl: null,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
||||||
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region 自分の情報の更新(i/pin, i/unpin)
|
//#region 自分の情報の更新(i/pin, i/unpin)
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||||
isBlocking: false,
|
isBlocking: false,
|
||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: false,
|
isCat: false,
|
||||||
|
speakAsCat: false,
|
||||||
isFollowed: false,
|
isFollowed: false,
|
||||||
isFollowing: false,
|
isFollowing: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
|
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
|
||||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
|
<link rel="preload" href="https://github.com/misskey-dev/misskey/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">
|
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"broadcast-channel": "5.3.0",
|
"broadcast-channel": "5.3.0",
|
||||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"canvas-confetti": "1.6.0",
|
"canvas-confetti": "1.6.1",
|
||||||
"chart.js": "4.4.0",
|
"chart.js": "4.4.0",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
"cropperjs": "2.0.0-beta.4",
|
"cropperjs": "2.0.0-beta.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"gsap": "3.12.2",
|
"gsap": "3.12.2",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
|
@ -57,12 +57,12 @@
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.3.0",
|
"punycode": "2.3.0",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rollup": "3.29.2",
|
"rollup": "3.29.4",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"sass": "1.68.0",
|
"sass": "1.68.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.156.1",
|
"three": "0.157.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"uuid": "9.0.1",
|
"uuid": "9.0.1",
|
||||||
"v-code-diff": "^1.7.1",
|
"v-code-diff": "1.7.1",
|
||||||
"vanilla-tilt": "1.8.1",
|
"vanilla-tilt": "1.8.1",
|
||||||
"vite": "4.4.9",
|
"vite": "4.4.9",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
|
@ -78,44 +78,44 @@
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-actions": "7.4.4",
|
"@storybook/addon-actions": "7.4.5",
|
||||||
"@storybook/addon-essentials": "7.4.4",
|
"@storybook/addon-essentials": "7.4.5",
|
||||||
"@storybook/addon-interactions": "7.4.4",
|
"@storybook/addon-interactions": "7.4.5",
|
||||||
"@storybook/addon-links": "7.4.4",
|
"@storybook/addon-links": "7.4.5",
|
||||||
"@storybook/addon-storysource": "7.4.4",
|
"@storybook/addon-storysource": "7.4.5",
|
||||||
"@storybook/addons": "7.4.4",
|
"@storybook/addons": "7.4.5",
|
||||||
"@storybook/blocks": "7.4.4",
|
"@storybook/blocks": "7.4.5",
|
||||||
"@storybook/core-events": "7.4.4",
|
"@storybook/core-events": "7.4.5",
|
||||||
"@storybook/jest": "0.2.2",
|
"@storybook/jest": "0.2.2",
|
||||||
"@storybook/manager-api": "7.4.4",
|
"@storybook/manager-api": "7.4.5",
|
||||||
"@storybook/preview-api": "7.4.4",
|
"@storybook/preview-api": "7.4.5",
|
||||||
"@storybook/react": "7.4.4",
|
"@storybook/react": "7.4.5",
|
||||||
"@storybook/react-vite": "7.4.4",
|
"@storybook/react-vite": "7.4.5",
|
||||||
"@storybook/testing-library": "0.2.1",
|
"@storybook/testing-library": "0.2.1",
|
||||||
"@storybook/theming": "7.4.4",
|
"@storybook/theming": "7.4.5",
|
||||||
"@storybook/types": "7.4.4",
|
"@storybook/types": "7.4.5",
|
||||||
"@storybook/vue3": "7.4.4",
|
"@storybook/vue3": "7.4.5",
|
||||||
"@storybook/vue3-vite": "7.4.4",
|
"@storybook/vue3-vite": "7.4.5",
|
||||||
"@testing-library/vue": "7.0.0",
|
"@testing-library/vue": "7.0.0",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/estree": "1.0.2",
|
"@types/estree": "1.0.2",
|
||||||
"@types/matter-js": "0.19.0",
|
"@types/matter-js": "0.19.1",
|
||||||
"@types/micromatch": "4.0.2",
|
"@types/micromatch": "4.0.3",
|
||||||
"@types/node": "20.6.4",
|
"@types/node": "20.7.1",
|
||||||
"@types/punycode": "2.1.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/throttle-debounce": "5.0.0",
|
||||||
"@types/tinycolor2": "1.4.4",
|
"@types/tinycolor2": "1.4.4",
|
||||||
"@types/uuid": "9.0.4",
|
"@types/uuid": "9.0.4",
|
||||||
"@types/websocket": "1.0.6",
|
"@types/websocket": "1.0.7",
|
||||||
"@types/ws": "8.5.5",
|
"@types/ws": "8.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.3",
|
||||||
"@vitest/coverage-v8": "0.34.5",
|
"@vitest/coverage-v8": "0.34.5",
|
||||||
"@vue/runtime-core": "3.3.4",
|
"@vue/runtime-core": "3.3.4",
|
||||||
"acorn": "8.10.0",
|
"acorn": "8.10.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.2.0",
|
"cypress": "13.3.0",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
"eslint-plugin-import": "2.28.1",
|
"eslint-plugin-import": "2.28.1",
|
||||||
"eslint-plugin-vue": "9.17.0",
|
"eslint-plugin-vue": "9.17.0",
|
||||||
|
@ -129,13 +129,13 @@
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"start-server-and-test": "2.0.1",
|
"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",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "0.34.5",
|
"vitest": "0.34.5",
|
||||||
"vitest-fetch-mock": "0.2.2",
|
"vitest-fetch-mock": "0.2.2",
|
||||||
"vue-eslint-parser": "9.3.1",
|
"vue-eslint-parser": "9.3.1",
|
||||||
"vue-tsc": "1.8.13"
|
"vue-tsc": "1.8.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
import { mainRouter } from '@/router.js';
|
import { mainRouter } from '@/router.js';
|
||||||
|
|
||||||
export async function common(createVue: () => App<Element>) {
|
export async function common(createVue: () => App<Element>) {
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Sharkey v${version}`);
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.warn('Development mode!!!');
|
console.warn('Development mode!!!');
|
||||||
|
|
|
@ -42,7 +42,7 @@ const toggle = () => {
|
||||||
.root {
|
.root {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.7em;
|
font-size: 0.76em;
|
||||||
color: var(--cwFg);
|
color: var(--cwFg);
|
||||||
background: var(--cwBg);
|
background: var(--cwBg);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
|
@ -22,6 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||||
<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
|
<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="$i?.backgroundId == file.id" :class="[$style.label]">
|
||||||
|
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||||
|
<p :class="$style.labelText">Background</p>
|
||||||
|
</div>
|
||||||
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
||||||
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
||||||
<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
|
<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
|
||||||
|
|
473
packages/frontend/src/components/MkMfmWindow.vue
Normal file
473
packages/frontend/src/components/MkMfmWindow.vue
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
<template>
|
||||||
|
<MkWindow
|
||||||
|
ref="window"
|
||||||
|
:initialWidth="600"
|
||||||
|
:initialHeight="800"
|
||||||
|
:canResize="true"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
MFM Cheatsheet
|
||||||
|
</template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<MkSpacer :contentMax="800">
|
||||||
|
<div class="mfm-cheat-sheet">
|
||||||
|
<div>{{ i18n.ts._mfm.intro }}</div>
|
||||||
|
<br />
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.mention }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.mentionDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_mention" />
|
||||||
|
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.hashtag }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.hashtagDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_hashtag" />
|
||||||
|
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.link }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.linkDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_link" />
|
||||||
|
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.emoji }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.emojiDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_emoji" />
|
||||||
|
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.bold }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.boldDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_bold" />
|
||||||
|
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.small }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.smallDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_small" />
|
||||||
|
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.quote }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.quoteDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_quote" />
|
||||||
|
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.center }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.centerDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_center" />
|
||||||
|
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.inlineCode }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.inlineCodeDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_inlineCode" />
|
||||||
|
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.blockCode }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.blockCodeDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_blockCode" />
|
||||||
|
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.inlineMath }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.inlineMathDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_inlineMath" />
|
||||||
|
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_blockMath" />
|
||||||
|
<MkTextarea v-model="preview_blockMath"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.search }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.searchDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_search" />
|
||||||
|
<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.flip }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.flipDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_flip" />
|
||||||
|
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.font }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.fontDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_font" />
|
||||||
|
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.x2 }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.x2Description }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_x2" />
|
||||||
|
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.x3 }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.x3Description }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_x3" />
|
||||||
|
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.x4 }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.x4Description }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_x4" />
|
||||||
|
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.blur }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.blurDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_blur" />
|
||||||
|
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.jelly }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.jellyDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_jelly" />
|
||||||
|
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.tada }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.tadaDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_tada" />
|
||||||
|
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.jump }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.jumpDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_jump" />
|
||||||
|
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.bounce }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.bounceDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_bounce" />
|
||||||
|
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.spin }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.spinDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_spin" />
|
||||||
|
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.shake }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.shakeDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_shake" />
|
||||||
|
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.twitch }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.twitchDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_twitch" />
|
||||||
|
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.rainbow }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.rainbowDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_rainbow" />
|
||||||
|
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.sparkle }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.sparkleDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_sparkle" />
|
||||||
|
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.rotate }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.rotateDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_rotate" />
|
||||||
|
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.position }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.positionDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_position" />
|
||||||
|
<MkTextarea v-model="preview_position"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.scale }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.scaleDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_scale" />
|
||||||
|
<MkTextarea v-model="preview_scale"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.foregroundDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_fg" />
|
||||||
|
<MkTextarea v-model="preview_fg"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.background }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.backgroundDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_bg" />
|
||||||
|
<MkTextarea v-model="preview_bg"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.plain }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.plainDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_plain" />
|
||||||
|
<MkTextarea v-model="preview_plain"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</MkWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import { i18n } from "@/i18n.js";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const preview_mention = ref("@example");
|
||||||
|
const preview_hashtag = ref("#test");
|
||||||
|
const preview_link = ref(`[${i18n.ts._mfm.dummy}](https://joinsharkey.org)`);
|
||||||
|
const preview_emoji = ref(`:heart:`);
|
||||||
|
const preview_bold = ref(`**${i18n.ts._mfm.dummy}**`);
|
||||||
|
const preview_small = ref(
|
||||||
|
`<small>${i18n.ts._mfm.dummy}</small>`,
|
||||||
|
);
|
||||||
|
const preview_center = ref(
|
||||||
|
`<center>${i18n.ts._mfm.dummy}</center>`,
|
||||||
|
);
|
||||||
|
const preview_inlineCode = ref('`<: "Hello, world!"`');
|
||||||
|
const preview_blockCode = ref(
|
||||||
|
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
||||||
|
);
|
||||||
|
const preview_inlineMath = ref(
|
||||||
|
"\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)",
|
||||||
|
);
|
||||||
|
const preview_blockMath = ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
|
||||||
|
const preview_quote = ref(`> ${i18n.ts._mfm.dummy}`);
|
||||||
|
const preview_search = ref(
|
||||||
|
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`,
|
||||||
|
);
|
||||||
|
const preview_jelly = ref(
|
||||||
|
"$[jelly 🍮] $[jelly.speed=3s 🍮] $[jelly.delay=3s 🍮] $[jelly.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_tada = ref(
|
||||||
|
"$[tada 🍮] $[tada.speed=3s 🍮] $[tada.delay=3s 🍮] $[tada.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_jump = ref(
|
||||||
|
"$[jump 🍮] $[jump.speed=3s 🍮] $[jump.delay=3s 🍮] $[jump.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_bounce = ref(
|
||||||
|
"$[bounce 🍮] $[bounce.speed=3s 🍮] $[bounce.delay=3s 🍮] $[bounce.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_shake = ref(
|
||||||
|
"$[shake 🍮] $[shake.speed=3s 🍮] $[shake.delay=3s 🍮] $[shake.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_twitch = ref(
|
||||||
|
"$[twitch 🍮] $[twitch.speed=3s 🍮] $[twitch.delay=3s 🍮] $[twitch.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_spin = ref(
|
||||||
|
"$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=3s 🍮] $[spin.delay=3s 🍮] $[spin.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_flip = ref(
|
||||||
|
`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`,
|
||||||
|
);
|
||||||
|
const preview_font = ref(
|
||||||
|
`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]`,
|
||||||
|
);
|
||||||
|
const preview_x2 = ref("$[x2 🍮]");
|
||||||
|
const preview_x3 = ref("$[x3 🍮]");
|
||||||
|
const preview_x4 = ref("$[x4 🍮]");
|
||||||
|
const preview_blur = ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||||
|
const preview_rainbow = ref(
|
||||||
|
"$[rainbow 🍮] $[rainbow.speed=3s 🍮] $[rainbow.delay=3s 🍮] $[rainbow.loop=3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_sparkle = ref("$[sparkle 🍮]");
|
||||||
|
const preview_rotate = ref(
|
||||||
|
"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]",
|
||||||
|
);
|
||||||
|
const preview_position = ref("$[position.y=-1 🍮]\n$[position.x=-1 🍮]");
|
||||||
|
const preview_scale = ref(
|
||||||
|
"$[scale.x=1.3 🍮]\n$[scale.x=1.5,y=3 🍮]\n$[scale.y=0.3 🍮]",
|
||||||
|
);
|
||||||
|
const preview_fg = ref("$[fg.color=eb6f92 Text color]");
|
||||||
|
const preview_bg = ref("$[bg.color=31748f Background color]");
|
||||||
|
const preview_plain = ref(
|
||||||
|
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>",
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mfm-cheat-sheet {
|
||||||
|
> .section {
|
||||||
|
> .title {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
top: var(--stickyTop, 0px);
|
||||||
|
padding: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||||
|
backdrop-filter: var(--blur, blur(10px));
|
||||||
|
background-color: var(--X16);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .preview {
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -217,9 +217,12 @@ const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
||||||
|
const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
|
||||||
|
const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
|
||||||
|
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
|
||||||
const isLong = shouldCollapsed(appearNote);
|
const isLong = shouldCollapsed(appearNote);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
|
|
@ -94,6 +94,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div :class="$style.noteFooterInfo">
|
<div :class="$style.noteFooterInfo">
|
||||||
|
<div v-if="appearNote.updatedAt">
|
||||||
|
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
|
||||||
|
</div>
|
||||||
<MkA :to="notePage(appearNote)">
|
<MkA :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
@ -257,13 +260,16 @@ const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
||||||
|
const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
|
||||||
|
const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
|
||||||
|
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey 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>
|
|
@ -1,95 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and other misskey 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>
|
|
|
@ -30,11 +30,11 @@ import MkNote from '@/components/MkNote.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { notificationTypes } from '@/const';
|
import { notificationTypes } from '@/const.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
includeTypes?: typeof notificationTypes[number][];
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
@ -43,13 +43,12 @@ const pagination: Paging = {
|
||||||
endpoint: 'i/notifications' as const,
|
endpoint: 'i/notifications' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
includeTypes: props.includeTypes ?? undefined,
|
excludeTypes: props.excludeTypes ?? undefined,
|
||||||
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNotification = (notification) => {
|
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') {
|
if (isMuted || document.visibilityState === 'visible') {
|
||||||
useStream().send('readNotification');
|
useStream().send('readNotification');
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||||
|
<span v-if="!isLocal"><span> · </span><a @click.stop="refresh">{{ i18n.ts.reload }}</a></span>
|
||||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,6 +45,7 @@ const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
|
const isLocal = computed(() => !props.note.uri);
|
||||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
||||||
const timer = computed(() => i18n.t(
|
const timer = computed(() => i18n.t(
|
||||||
remaining.value >= 86400 ? '_poll.remainingDays' :
|
remaining.value >= 86400 ? '_poll.remainingDays' :
|
||||||
|
@ -89,6 +91,14 @@ const vote = async (id) => {
|
||||||
});
|
});
|
||||||
if (!showResult.value) showResult.value = !props.note.poll.multiple;
|
if (!showResult.value) showResult.value = !props.note.poll.multiple;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (!props.note.uri) return;
|
||||||
|
const obj = await os.apiWithDialog("ap/show", { uri: props.note.uri });
|
||||||
|
if (obj.type === "Note" && obj.object.poll) {
|
||||||
|
props.note.poll = obj.object.poll; // eslint-disable-line vue/no-mutating-props
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -88,6 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.footerRight">
|
<div :class="$style.footerRight">
|
||||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button>
|
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button>
|
||||||
|
<button v-tooltip="'MFM Cheatsheet'" class="_button" :class="$style.footerButton" @click="MFMWindow"><i class="ph-notebook ph-bold ph-lg"></i></button>
|
||||||
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ph-dots-three ph-bold ph-lg"></i></button>-->
|
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ph-dots-three ph-bold ph-lg"></i></button>-->
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -346,6 +347,10 @@ function watchForDraft() {
|
||||||
watch($$(localOnly), () => saveDraft());
|
watch($$(localOnly), () => saveDraft());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MFMWindow() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkMfmWindow.vue')), {}, {}, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
function checkMissingMention() {
|
function checkMissingMention() {
|
||||||
if (visibility === 'specified') {
|
if (visibility === 'specified') {
|
||||||
const ast = mfm.parse(text);
|
const ast = mfm.parse(text);
|
||||||
|
@ -699,13 +704,13 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let postData = {
|
let postData = {
|
||||||
text: text === '' ? undefined : text,
|
text: text === '' ? null : text,
|
||||||
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
|
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
|
||||||
replyId: props.reply ? props.reply.id : undefined,
|
replyId: props.reply ? props.reply.id : undefined,
|
||||||
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
|
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: props.channel ? props.channel.id : undefined,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
cw: useCw ? cw ?? '' : undefined,
|
cw: useCw ? cw ?? '' : null,
|
||||||
localOnly: localOnly,
|
localOnly: localOnly,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
|
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
|
||||||
|
@ -1159,7 +1164,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerRight {
|
.footerRight {
|
||||||
flex: 0;
|
flex: 0.3;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
|
|
|
@ -24,9 +24,11 @@ const props = withDefaults(defineProps<{
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
|
onlyFiles?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
|
onlyFiles: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -69,10 +71,12 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('homeTimeline', {
|
connection = stream.useChannel('homeTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
|
||||||
|
@ -82,10 +86,12 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('localTimeline', {
|
connection = stream.useChannel('localTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
} else if (props.src === 'social') {
|
} else if (props.src === 'social') {
|
||||||
|
@ -93,10 +99,12 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
} else if (props.src === 'global') {
|
} else if (props.src === 'global') {
|
||||||
|
@ -104,10 +112,12 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('globalTimeline', {
|
connection = stream.useChannel('globalTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
} else if (props.src === 'mentions') {
|
} else if (props.src === 'mentions') {
|
||||||
|
@ -131,11 +141,13 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('userList', {
|
connection = stream.useChannel('userList', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
|
|
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="user != null">
|
<div v-if="user != null">
|
||||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
||||||
|
<span v-if="user.isLocked && $i && $i.id != user.id && !user.isFollowing" :title="i18n.ts.isLocked" :class="$style.locked"><i class="ph-lock ph-bold ph-lg"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
|
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
|
||||||
<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
|
<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
|
||||||
|
@ -148,6 +149,28 @@ onMounted(() => {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.locked:first-child {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: 0.7em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked:not(:first-child) {
|
||||||
|
position: absolute;
|
||||||
|
top: 34px;
|
||||||
|
left: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: 0.7em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.avatarBack {
|
.avatarBack {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const ROLE_POLICIES = [
|
||||||
'inviteExpirationTime',
|
'inviteExpirationTime',
|
||||||
'canManageCustomEmojis',
|
'canManageCustomEmojis',
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
|
'canUseTranslator',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
'driveCapacityMb',
|
'driveCapacityMb',
|
||||||
'alwaysMarkNsfw',
|
'alwaysMarkNsfw',
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||||
|
|
||||||
import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api.js';
|
import { pendingApiRequestsCount, api, apiExternal, apiGet } from '@/scripts/api.js';
|
||||||
export { pendingApiRequestsCount, api, apiGet };
|
export { pendingApiRequestsCount, api, apiExternal, apiGet };
|
||||||
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
|
|
|
@ -5,8 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header>
|
||||||
|
<XHeader :actions="headerActions" :tabs="headerTabs" />
|
||||||
|
</template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
|
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
||||||
|
{{ i18n.ts.publishing }}
|
||||||
|
</MkSwitch>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||||
<MkAd v-if="ad.url" :specify="ad" />
|
<MkAd v-if="ad.url" :specify="ad" />
|
||||||
|
@ -46,7 +51,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span>
|
<span>
|
||||||
{{ i18n.ts._ad.timezoneinfo }}
|
{{ i18n.ts._ad.timezoneinfo }}
|
||||||
<div v-for="(day, index) in daysOfWeek" :key="index">
|
<div v-for="(day, index) in daysOfWeek" :key="index">
|
||||||
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0" @change="toggleDayOfWeek(ad, index)">
|
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
||||||
|
@change="toggleDayOfWeek(ad, index)">
|
||||||
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -75,6 +81,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -86,8 +93,9 @@ let ads: any[] = $ref([]);
|
||||||
const localTime = new Date();
|
const localTime = new Date();
|
||||||
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||||
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
||||||
|
let publishing = false;
|
||||||
|
|
||||||
os.api('admin/ad/list').then(adsResponse => {
|
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
ads = adsResponse.map(r => {
|
ads = adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
@ -101,6 +109,10 @@ os.api('admin/ad/list').then(adsResponse => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onChangePublishing = (v) => {
|
||||||
|
publishing = v;
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
// 選択された曜日(index)のビットフラグを操作する
|
// 選択された曜日(index)のビットフラグを操作する
|
||||||
function toggleDayOfWeek(ad, index) {
|
function toggleDayOfWeek(ad, index) {
|
||||||
ad.dayOfWeek ^= 1 << index;
|
ad.dayOfWeek ^= 1 << index;
|
||||||
|
@ -131,6 +143,8 @@ function remove(ad) {
|
||||||
if (ad.id == null) return;
|
if (ad.id == null) return;
|
||||||
os.apiWithDialog('admin/ad/delete', {
|
os.apiWithDialog('admin/ad/delete', {
|
||||||
id: ad.id,
|
id: ad.id,
|
||||||
|
}).then(() => {
|
||||||
|
refresh();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -172,7 +186,7 @@ function save(ad) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function more() {
|
function more() {
|
||||||
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id }).then(adsResponse => {
|
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
||||||
ads = ads.concat(adsResponse.map(r => {
|
ads = ads.concat(adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
@ -188,7 +202,7 @@ function more() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
os.api('admin/ad/list').then(adsResponse => {
|
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
ads = adsResponse.map(r => {
|
ads = adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
|
|
@ -6,18 +6,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
<template #label>
|
<template #label>
|
||||||
<b>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
<b
|
||||||
|
:class="{
|
||||||
|
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
|
||||||
|
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||||
|
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
|
||||||
|
}"
|
||||||
|
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
|
||||||
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
|
||||||
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
|
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
|
||||||
<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
|
<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
|
||||||
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
|
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
|
||||||
<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
||||||
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
|
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
|
||||||
|
<span v-else-if="log.type === 'deleteCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
||||||
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||||
|
@ -76,6 +83,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="log.type === 'updateAd'">
|
||||||
|
<div :class="$style.diff">
|
||||||
|
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>raw</summary>
|
<summary>raw</summary>
|
||||||
|
@ -114,4 +126,16 @@ const props = defineProps<{
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logYellow {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logRed {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logGreen {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -279,6 +279,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canUseTranslator.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canUseTranslator.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseTranslator)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canUseTranslator.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canUseTranslator.value" :disabled="role.policies.canUseTranslator.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canUseTranslator.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -95,6 +95,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||||
|
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.canUseTranslator">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||||
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<div v-if="tab === 'all'">
|
<div v-if="tab === 'all'">
|
||||||
<XNotifications class="notifications" :includeTypes="includeTypes"/>
|
<XNotifications class="notifications" :excludeTypes="excludeTypes"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'mentions'">
|
<div v-else-if="tab === 'mentions'">
|
||||||
<MkNotes :pagination="mentionsPagination"/>
|
<MkNotes :pagination="mentionsPagination"/>
|
||||||
|
@ -27,10 +27,11 @@ import MkNotes from '@/components/MkNotes.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { notificationTypes } from '@/const';
|
import { notificationTypes } from '@/const.js';
|
||||||
|
|
||||||
let tab = $ref('all');
|
let tab = $ref('all');
|
||||||
let includeTypes = $ref<string[] | null>(null);
|
let includeTypes = $ref<string[] | null>(null);
|
||||||
|
const excludeTypes = $computed(() => includeTypes ? notificationTypes.filter(t => !includeTypes.includes(t)) : null);
|
||||||
|
|
||||||
const mentionsPagination = {
|
const mentionsPagination = {
|
||||||
endpoint: 'notes/mentions' as const,
|
endpoint: 'notes/mentions' as const,
|
||||||
|
|
|
@ -83,6 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #value><code class="_monospace">{{ code }}</code></template>
|
<template #value><code class="_monospace">{{ code }}</code></template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,6 +110,7 @@ import * as os from '@/os.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
twoFactorData: {
|
twoFactorData: {
|
||||||
|
@ -143,6 +146,16 @@ async function tokenDone() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadBackupCodes() {
|
||||||
|
if (backupCodes.value !== undefined) {
|
||||||
|
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
|
||||||
|
const dummya = document.createElement('a');
|
||||||
|
dummya.href = URL.createObjectURL(txtBlob);
|
||||||
|
dummya.download = `${$i?.username}-2fa-backup-codes.txt`;
|
||||||
|
dummya.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function allDone() {
|
function allDone() {
|
||||||
dialog.value.close();
|
dialog.value.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ const menuDef = computed(() => [{
|
||||||
to: '/settings/roles',
|
to: '/settings/roles',
|
||||||
active: currentPage?.route.name === 'roles',
|
active: currentPage?.route.name === 'roles',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ph-prohibit ph-bold pg-lg',
|
icon: 'ph-speaker-none ph-bold pg-lg',
|
||||||
text: i18n.ts.instanceMute,
|
text: i18n.ts.instanceMute,
|
||||||
to: '/settings/instance-mute',
|
to: '/settings/instance-mute',
|
||||||
active: currentPage?.route.name === 'instance-mute',
|
active: currentPage?.route.name === 'instance-mute',
|
||||||
|
@ -150,12 +150,12 @@ const menuDef = computed(() => [{
|
||||||
to: '/settings/mute-block',
|
to: '/settings/mute-block',
|
||||||
active: currentPage?.route.name === 'mute-block',
|
active: currentPage?.route.name === 'mute-block',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ph-bell-slash ph-bold ph-lg',
|
icon: 'ph-speaker-x ph-bold ph-lg',
|
||||||
text: i18n.ts.wordMute,
|
text: i18n.ts.wordMute,
|
||||||
to: '/settings/word-mute',
|
to: '/settings/word-mute',
|
||||||
active: currentPage?.route.name === 'word-mute',
|
active: currentPage?.route.name === 'word-mute',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ph-webhooks-logo ph-bold pg-lg',
|
icon: 'ph-key ph-bold pg-lg',
|
||||||
text: 'API',
|
text: 'API',
|
||||||
to: '/settings/api',
|
to: '/settings/api',
|
||||||
active: currentPage?.route.name === 'api',
|
active: currentPage?.route.name === 'api',
|
||||||
|
|
|
@ -5,13 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkTab v-model="tab">
|
<MkFolder>
|
||||||
<option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option>
|
<template #icon><i class="ti ti-repeat-off"></i></template>
|
||||||
<option value="mute">{{ i18n.ts.mutedUsers }}</option>
|
<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
|
||||||
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
|
||||||
</MkTab>
|
|
||||||
|
|
||||||
<div v-if="tab === 'renoteMute'">
|
|
||||||
<MkPagination :pagination="renoteMutingPagination">
|
<MkPagination :pagination="renoteMutingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -37,9 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.mutedUsers }}</template>
|
||||||
|
|
||||||
<div v-else-if="tab === 'mute'">
|
|
||||||
<MkPagination :pagination="mutingPagination">
|
<MkPagination :pagination="mutingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -67,9 +67,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-ban"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.blockedUsers }}</template>
|
||||||
|
|
||||||
<div v-else-if="tab === 'block'">
|
|
||||||
<MkPagination :pagination="blockingPagination">
|
<MkPagination :pagination="blockingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -97,24 +100,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
|
||||||
import FormLink from '@/components/form/link.vue';
|
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
let tab = $ref('renoteMute');
|
|
||||||
|
|
||||||
const renoteMutingPagination = {
|
const renoteMutingPagination = {
|
||||||
endpoint: 'renote-mute/list' as const,
|
endpoint: 'renote-mute/list' as const,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue