merge: Merge Upstream Changes (!408)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/408

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
This commit is contained in:
Marie 2024-02-12 23:14:55 +00:00
commit 4007fbb8d8
149 changed files with 1936 additions and 697 deletions

View File

@ -24,6 +24,8 @@
- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
* すべてのリモートユーザーのリアクション一覧を見えないようにします
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
- Fix: 特定のキーワードを含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207
* デフォルトは空欄なので適用前と同等の動作になります
### Client
- Feat: 新しいゲームを追加
@ -49,6 +51,12 @@
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
- ロールが必要な絵文字をリアクションしようとした場合
- Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
@ -62,6 +70,13 @@
- Enhance: ページ遷移時にPlayerを閉じるように
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正
- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
- Fix: 画像をクロップ時、正常に完了できない問題の修正
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
- Fix: エラー画像URLを設定した後解除するとデフォルトの画像が表示されない問題の修正
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
### Server
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
@ -76,6 +91,7 @@
- Fix: properly handle cc followers
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
- Enhance: 連合向けのノート配信を軽量化 #13192
### Service Worker
- Enhance: オフライン表示のデザインを改善・多言語対応

View File

@ -286,18 +286,17 @@ export const argTypes = {
min: 1,
max: 4,
},
},
};
```
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
```ts
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
export const handlers = [
rest.post('/api/notes/timeline', (req, res, ctx) => {
return res(
ctx.json([]),
);
http.post('/api/notes/timeline', ({ request }) => {
return HttpResponse.json([]);
}),
];
```

View File

@ -1041,6 +1041,9 @@ resetPasswordConfirm: "Vols canviar la teva contrasenya?"
sensitiveWords: "Paraules sensibles"
sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular."
prohibitedWords: "Paraules prohibides"
prohibitedWordsDescription: "Quan intenteu publicar una Nota que conté una paraula prohibida, feu que es converteixi en un error. Es poden dividir i establir múltiples línies."
prohibitedWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular."
hiddenTags: "Etiquetes ocultes"
hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
notesSearchNotAvailable: "La cerca de notes no es troba disponible."
@ -1157,6 +1160,12 @@ edited: "Editat"
notificationRecieveConfig: "Paràmetres de notificacions"
mutualFollow: "Seguidor mutu"
fileAttachedOnly: "Només notes amb adjunts"
showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la línia de temps"
hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la línia de temps"
showRepliesToOthersInTimelineAll: "Mostrar les respostes a altres a usuaris que segueixes a la línia de temps"
hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps"
confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?"
confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?"
externalServices: "Serveis externs"
impressum: "Impressum"
impressumUrl: "Adreça URL impressum"
@ -1187,7 +1196,25 @@ seasonalScreenEffect: "Efectes de pantalla segons les estacions"
decorate: "Decorar"
addMfmFunction: "Afegeix funcions MFM"
enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM"
bubbleGame: "Bubble Game"
sfx: "Efectes de so"
soundWillBePlayed: "Es reproduiran efectes de so"
showReplay: "Veure reproducció"
replay: "Reproduir"
replaying: "Reproduint"
ranking: "Classificació"
lastNDays: "Últims {n} dies"
backToTitle: "Torna al títol"
hemisphere: "Geolocalització"
withSensitive: "Incloure notes amb fitxers sensibles"
userSaysSomethingSensitive: "La publicació de {name} conte material sensible"
enableHorizontalSwipe: "Lliscar per canviar de pestanya"
_bubbleGame:
howToPlay: "Com es juga"
_howToPlay:
section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa."
section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts."
section3: "El joc s'acabarà quan els objectes sobresurtin de la caixa. Intenta aconseguir la puntuació més gran possible fusionant objectes mentre impedeixes que sobresurtin de la caixa!"
_announcement:
forExistingUsers: "Anunci per usuaris registrats"
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
@ -1209,8 +1236,32 @@ _initialAccountSetting:
privacySetting: "Configuració de seguretat"
theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard."
youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard."
followUsers: "Prova de seguir usuaris que t'interessin per construir la teva línia de temps."
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
initialAccountSettingCompleted: "Configuració del perfil completada!"
haveFun: "Disfruta {name}!"
youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (MissKey) o tu pots estalviar i començar a fer-lo servir ja."
startTutorial: "Començar el tutorial"
skipAreYouSure: "Et vols saltar la configuració del perfil?"
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
_initialTutorial:
launchTutorial: "Començar tutorial"
title: "Tutorial"
wellDone: "Ben fet!"
skipAreYouSure: "Sortir del tutorial?"
_landing:
title: "Benvingut al tutorial"
description: "Aquí aprendràs el bàsic per poder fer servir Misskey i les seves característiques."
_note:
title: "Què és una Nota?"
description: "Les publicacions a Misskey es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica."
reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil."
renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris."
reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina."
menu: "Pots veure els detalls de les Notes, copiar enllaços i fer diferents accions."
_reaction:
title: "Què són les Reaccions?"
description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada."
letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!"
reactToContinue: "Afegeix una reacció per continuar."
reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes."
@ -1272,9 +1323,75 @@ _serverSettings:
shortName: "Nom curt"
shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg"
fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
fanoutTimelineDbFallback: "Carregar de la base de dades"
fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir."
_accountMigration:
moveFrom: "Migrar un altre compte a aquest"
moveFromSub: "Crear un àlies per un altre compte"
moveFromLabel: "Compte original #{n}"
moveFromDescription: "Has de crear un àlies del compte que vols migrar en aquest compte.\nFes servir aquest format per posar el compte que vols migrar: @nomusuari@servidor.exemple.com\nPer esborrar l'àlies deixa el camp en blanc (no és recomanable de fer)"
moveTo: "Migrar aquest compte a un altre"
moveToLabel: "Compte al qual es vol migrar:"
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
startMigration: "Migrar"
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer."
postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de germinar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte."
movedTo: "Nou compte:"
_achievements:
earnedAt: "Desbloquejat el"
_types:
_notes1:
title: "Aquí, configurant el meu msky"
description: "Publica la teva primera Nota"
flavor: "Passa-t'ho bé fent servir Miskey!"
_notes10:
title: "Algunes notes"
description: "Publica 10 notes"
_notes100:
title: "Un piló de notes"
description: "Publica 100 notes"
_notes500:
title: "Cobert de notes"
description: "Publica 500 notes"
_notes1000:
title: "Un piló de notes"
description: "1 000 notes publicades"
_notes5000:
title: "Desbordament de notes"
description: "5 000 notes publicades"
_notes10000:
title: "Supernota"
description: "10 000 notes publicades"
_notes20000:
title: "Necessito... Més... Notes!"
description: "20 000 notes publicades"
_notes30000:
title: "Notes notes notes!"
description: "30 000 notes publicades"
_notes40000:
title: "Fàbrica de notes"
description: "40 000 notes publicades"
_notes50000:
title: "Planeta de notes"
description: "50 000 notes publicades"
_notes60000:
title: "Quàsar de notes"
description: "60 000 notes publicades"
_notes70000:
title: "Forat negre de notes"
description: "70 000 notes publicades"
_notes80000:
title: "Galàxia de notes"
description: "80 000 notes publicades"
_notes90000:
title: "Univers de notes"
description: "90 000 notes publicades"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "100 000 notes publicades"
flavor: "Segur que tens moltes coses a dir?"
_login3:
title: "Principiant I"
@ -1347,42 +1464,413 @@ _achievements:
description: "És la primera vegada que et segueixo"
_following10:
title: "Segueix-me... Segueix-me..."
description: "Seguir 10 usuaris"
_following50:
title: "Molts amics"
description: "Seguir 50 comptes"
_following100:
title: "100 amics"
description: "Segueixes 100 comptes"
_following300:
title: "Sobrecàrrega d'amics"
description: "Segueixes 300 comptes"
_followers1:
title: "Primer seguidor"
description: "1 seguidor guanyat"
_followers10:
title: "Segueix-me!"
description: "10 seguidors guanyats"
_followers50:
title: "Venen en manada"
description: "50 seguidors guanyats"
_followers100:
title: "Popular"
description: "100 seguidors guanyats"
_followers300:
title: "Si us plau, d'un en un!"
description: "300 seguidors guanyats"
_followers500:
title: "Torre de ràdio"
description: "500 seguidors guanyats"
_followers1000:
title: "Influenciador"
description: "1 000 seguidors guanyats"
_collectAchievements30:
title: "Col·leccionista d'èxits "
description: "Desbloqueja 30 assoliments"
_viewAchievements3min:
title: "M'agraden els èxits "
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
_iLoveMisskey:
title: "Estimo Misskey"
description: "Publica \"I ❤ #Misskey\""
flavor: "L'equip de desenvolupament de Misskey agraeix el vostre suport!"
_foundTreasure:
title: "A la Recerca del Tresor"
description: "Has trobat el tresor amagat"
_client30min:
title: "Parem una estona"
description: "Mantingues obert Misskey per 30 minuts"
_client60min:
title: "A totes amb Misskey"
description: "Mantingues Misskey obert per 60 minuts"
_noteDeletedWithin1min:
title: "No et preocupis"
description: "Esborra una nota al minut de publicar-la"
_postedAtLateNight:
title: "Nocturn"
description: "Publica una nota a altes hores de la nit "
flavor: "És hora d'anar a dormir."
_postedAt0min0sec:
title: "Rellotge xerraire"
description: "Publica una nota a les 0:00"
flavor: "Tic tac, tic tac, tic tac, DING!"
_selfQuote:
title: "Autoreferència "
description: "Cita una nota teva"
_htl20npm:
title: "Línia de temps fluida"
description: "La teva línia de temps va a més de 20npm (notes per minut)"
_viewInstanceChart:
title: "Analista "
description: "Mira els gràfics de la teva instància "
_outputHelloWorldOnScratchpad:
title: "Hola, món!"
description: "Escriu \"hola, món\" al bloc de notes"
_open3windows:
title: "Multi finestres"
description: "I va obrir més de tres finestres"
_driveFolderCircularReference:
title: "Consulteu la secció de bucle"
description: "Intenta crear carpetes recursives al Disc"
_reactWithoutRead:
title: "De veritat has llegit això?"
description: "Reaccions a una nota de més de 100 caràcters publicada fa menys de 3 segons "
_clickedClickHere:
title: "Fer clic"
description: "Has fet clic aquí "
_justPlainLucky:
title: "Ha sigut sort"
description: "Oportunitat de guanyar-lo amb una probabilitat d'un 0.005% cada 10 segons"
_setNameToSyuilo:
title: "soc millor"
description: "Posat \"siuylo\" com a nom"
_passedSinceAccountCreated1:
title: "Primer aniversari"
description: "Ja ha passat un any d'ençà que vas crear el teu compte"
_passedSinceAccountCreated2:
title: "Segon aniversari"
description: "Ja han passat dos anys d'ençà que vas crear el teu compte"
_passedSinceAccountCreated3:
title: "Tres anys"
description: "Ja han passat tres anys d'ençà que vas crear el teu compte"
_loggedInOnBirthday:
title: "Felicitats!"
description: "T'has identificat el dia del teu aniversari"
_loggedInOnNewYearsDay:
title: "Bon any nou!"
description: "T'has identificat el primer dia de l'any "
flavor: "A per un altre any memorable a la teva instància "
_cookieClicked:
title: "Un joc en què fas clic a les galetes"
description: "Pica galetes"
flavor: "Espera, ets al lloc web correcte?"
_brainDiver:
title: "Busseja Ments"
description: "Publica un enllaç al Busseja Ments"
flavor: "Misskey-Misskey La-Tu-Ma"
_smashTestNotificationButton:
title: "Sobrecàrrega de proves"
description: "Envia moltes notificacions de prova en un període de temps molt curt"
_tutorialCompleted:
title: "Diploma del Curs Elemental de Misskey"
description: "Has completat el tutorial"
_bubbleGameExplodingHead:
title: "🤯"
description: "L'objecte més gran del joc de la bombolla "
_bubbleGameDoubleExplodingHead:
title: "Doble 🤯"
description: "Dos dels objectes més grans del joc de la bombolla al mateix temps"
flavor: "Pots emplenar una carmanyola com aquesta 🤯🤯 una mica"
_role:
new: "Nou rol"
edit: "Editar el rol"
name: "Nom del rol"
description: "Descripció del rol"
permission: "Permisos de rol"
descriptionOfPermission: "Els <b>Moderadors</b> poden fer operacions bàsiques de moderació.\nEls <b>Administradors</b> poden canviar tots els ajustos del servidor."
assignTarget: "Assignar "
descriptionOfAssignTarget: "<b>Manual</b> per canviar manualment qui és part d'aquest rol i qui no.\n<b>Condicional</b> per afegir o eliminar de manera automàtica els usuaris d'aquest rol basat en una determinada condició."
manual: "Manual"
manualRoles: "Rols manuals"
conditional: "Condicional"
conditionalRoles: "Rols condicionals"
condition: "Condició"
isConditionalRole: "Aquest és un rol condicional"
isPublic: "Rol públic"
descriptionOfIsPublic: "Aquest rol es mostrarà al perfil dels usuaris al que se'ls assigni."
options: "Opcions"
policies: "Polítiques"
baseRole: "Plantilla de rols"
useBaseValue: "Fer servir els valors de la plantilla de rols"
chooseRoleToAssign: "Selecciona els rols a assignar"
iconUrl: "URL de la icona "
asBadge: "Mostrar com a insígnia "
descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol."
isExplorable: "Fer el rol explorable"
descriptionOfIsExplorable: "La línia de temps d'aquest rol i la llista d'usuaris seran públics si s'activa."
displayOrder: "Posició "
descriptionOfDisplayOrder: "Com més gran és el número, més dalt la seva posició a la interfície."
canEditMembersByModerator: "Permetre que els moderadors editin la llista d'usuaris en aquest rol"
descriptionOfCanEditMembersByModerator: "Quan s'activa, els moderadors, així com els administradors, podran afegir i treure usuaris d'aquest rol. Si es troba desactivat, només els administradors poden assignar usuaris."
priority: "Prioritat"
_priority:
low: "Baixa"
middle: "Mitjà"
high: "Alta"
_options:
gtlAvailable: "Pot veure la línia de temps global"
ltlAvailable: "Pot veure la línia de temps local"
canPublicNote: "Pot enviar notes públiques"
canInvite: "Pot crear invitacions a la instància "
inviteLimit: "Límit d'invitacions "
inviteLimitCycle: "Temps de refresc de les invitacions"
inviteExpirationTime: "Interval de caducitat de les invitacions"
canManageCustomEmojis: "Gestiona els emojis personalitzats"
canManageAvatarDecorations: "Gestiona les decoracions dels avatars "
driveCapacity: "Capacitat del disc"
alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles"
pinMax: "Nombre màxim de notes fixades"
antennaMax: "Nombre màxim d'antenes"
wordMuteMax: "Nombre màxim de caràcters permesos a les paraules silenciades"
webhookMax: "Nombre màxim de Webhooks"
clipMax: "Nombre màxim de clips"
noteEachClipsMax: "Nombre màxim de notes dintre d'un clip"
userListMax: "Nombre màxim de llistes d'usuaris "
userEachUserListsMax: "Nombre màxim d'usuaris dintre d'una llista d'usuaris "
rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límits baixos són menys restrictius, límits alts són més restrictius."
canHideAds: "Pot amagar els anuncis"
canSearchNotes: "Pot cercar notes"
canUseTranslator: "Pot fer servir el traductor"
avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars"
_condition:
isLocal: "Usuari local"
isRemote: "Usuari remot"
createdLessThan: "Han passat menys de X a passat des de la creació del compte"
createdMoreThan: "Han passat més de X des de la creació del compte"
followersLessThanOrEq: "Té menys de X seguidors"
followersMoreThanOrEq: "Té X o més seguidors"
followingLessThanOrEq: "Segueix X o menys comptes"
followingMoreThanOrEq: "Segueix a X o més comptes"
notesLessThanOrEq: "Les publicacions són menys o igual a "
notesMoreThanOrEq: "Les publicacions són més o igual a "
and: "AND condicional "
or: "OR condicional"
not: "NOT condicional"
_sensitiveMediaDetection:
description: "Redueix els esforços de moderació gràcies al reconeixement automàtic dels fitxers amb contingut sensible mitjançant Machine Learing. Això augmentarà la càrrega del servidor."
sensitivity: "Sensibilitat de la detecció "
sensitivityDescription: "Reduint la sensibilitat provocarà menys falsos positius. D'altra banda incrementant-ho generarà més falsos negatius."
setSensitiveFlagAutomatically: "Marcar com a sensible"
setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada."
analyzeVideos: "Activar anàlisis de vídeos "
analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor."
_emailUnavailable:
used: "Aquest correu electrònic ja s'està fent servir"
format: "El format del correu electrònic és invàlid "
disposable: "No es poden fer servir adreces de correu electrònic d'un sol ús "
mx: "Aquest servidor de correu electrònic no és vàlid "
smtp: "Aquest servidor de correu electrònic no respon"
banned: "No pots registrar-te amb aquesta adreça de correu electrònic "
_ffVisibility:
public: "Publicar"
followers: "Visible només per a seguidors "
private: "Privat"
_signup:
almostThere: "Ja quasi estem"
emailAddressInfo: "Si us plau, escriu la teva adreça de correu electrònic. No es farà pública."
emailSent: "S'ha enviat un correu de confirmació a ({email}). Si us plau, fes clic a l'enllaç per completar el registre."
_accountDelete:
accountDelete: "Eliminar el compte"
mayTakeTime: "Com l'eliminació d'un compte consumeix bastants recursos, pot trigar un temps perquè es completi l'esborrat, depenent si tens molt contingut i la quantitat de fitxer que hagis pujat."
sendEmail: "Una vegada hagi finalitzat l'esborrat del compte rebràs un correu electrònic a l'adreça que tinguis registrada en aquest compte."
requestAccountDelete: "Demanar l'eliminació del compte"
started: "Ha començat l'esborrat del compte."
inProgress: "L'esborrat es troba en procés "
_ad:
back: "Tornar"
reduceFrequencyOfThisAd: "Mostrar menys aquest anunci"
hide: "No mostrar mai"
timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor."
adsSettings: "Configuració d'anuncis "
notesPerOneAd: "Interval d'emplaçament d'anuncis en temps real (Notes per anuncis)"
setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real"
adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix."
_forgotPassword:
enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la."
ifNoEmail: "Si no vas fer servir una adreça de correu electrònic per registrar-te, si us plau posa't en contacte amb l'administrador."
contactAdmin: "Aquesta instància no suporta registrar-se amb correu electrònic. Si us plau, contacta amb l'administrador del servidor."
_gallery:
my: "La meva Galeria "
liked: "Publicacions que t'han agradat"
like: "M'agrada "
unlike: "Ja no m'agrada"
_email:
_follow:
title: "t'ha seguit"
_receiveFollowRequest:
title: "Has rebut una sol·licitud de seguiment"
_plugin:
install: "Instal·lar un afegit "
installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
manage: "Gestionar els afegits"
viewSource: "Veure l'origen "
_preferencesBackups:
list: "Llista de còpies de seguretat"
saveNew: "Fer una còpia de seguretat nova"
loadFile: "Carregar des d'un fitxer"
apply: "Aplicar en aquest dispositiu"
save: "Desar els canvis"
inputName: "Escriu un nom per aquesta còpia de seguretat"
cannotSave: "No s'ha pogut desar"
nameAlreadyExists: "Ja existeix una còpia de seguretat anomenada \"{name}\". Escriu un nom diferent."
applyConfirm: "Vols aplicar la còpia de seguretat \"{name}\" a aquest dispositiu? La configuració actual del dispositiu serà esborrada."
saveConfirm: "Desar còpia de seguretat com {name}?"
deleteConfirm: "Esborrar la còpia de seguretat {name}?"
renameConfirm: "Vols canvia el nom de la còpia de seguretat de \"{old}\" a \"{new}\"?"
noBackups: "No hi ha còpies de seguretat. Pots fer una còpia de seguretat de la configuració d'aquest dispositiu al servidor fent servir \"Crear nova còpia de seguretat\""
createdAt: "Creat el: {date} {time}"
updatedAt: "Actualitzat el: {date} {time}"
cannotLoad: "Hi ha hagut un error al carregar"
invalidFile: "Format del fitxer no vàlid "
_registry:
scope: "Àmbit "
key: "Clau"
keys: "Claus"
domain: "Domini"
createKey: "Crear una clau"
_aboutMisskey:
about: "Misskey és un programa de codi obert desenvolupar per syuilo des de 2014"
contributors: "Col·laboradors principals"
allContributors: "Tots els col·laboradors "
source: "Codi font"
translation: "Tradueix Misskey"
donate: "Fes un donatiu a Misskey"
morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Gràcies! 🥰"
patrons: "Patrocinadors"
projectMembers: "Membres del projecte"
_displayOfSensitiveMedia:
respect: "Ocultar imatges o vídeos marcats com a sensibles"
ignore: "Mostrar imatges o vídeos marcats com a sensibles"
force: "Ocultar totes les imatges o vídeos "
_instanceTicker:
none: "No mostrar mai"
remote: "Mostrar per usuaris remots"
always: "Mostrar sempre"
_serverDisconnectedBehavior:
reload: "Recarregar automàticament "
dialog: "Mostrar finestres de confirmació "
quiet: "Mostrar un avís que no molesti"
_channel:
create: "Crear un canal"
edit: "Editar canal"
setBanner: "Estableix el bàner "
removeBanner: "Eliminar el.bàner"
featured: "Popular"
owned: "Propietat"
following: "Seguin"
usersCount: "{n} Participants"
notesCount: "{n} Notes"
nameAndDescription: "Nom i descripció "
nameOnly: "Nom només "
allowRenoteToExternal: "Permet la citació i l'impuls fora del canal"
_instanceMute:
instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat."
_theme:
description: "Descripció"
keys:
navHoverFg: "Text barra lateral (en passar per sobre)"
navActive: "Text barra lateral (actiu)"
navIndicator: "Indicador barra lateral"
link: "Enllaç"
hashtag: "Etiqueta"
mention: "Menció"
mentionMe: "Mencions (jo)"
renote: "Renotar"
modalBg: "Fons del modal"
divider: "Divisor"
scrollbarHandle: "Maneta de la barra de desplaçament"
scrollbarHandleHover: "Maneta de la barra de desplaçament (en passar-hi per sobre)"
dateLabelFg: "Text de l'etiqueta de la data"
infoBg: "Fons d'informació "
infoFg: "Text d'informació "
infoWarnBg: "Fons avís "
infoWarnFg: "Text avís "
toastBg: "Fons notificació "
toastFg: "Text notificació "
buttonBg: "Fons botó "
buttonHoverBg: "Fons botó (en passar-hi per sobre)"
inputBorder: "Contorn del cap d'introducció "
listItemHoverBg: "Fons dels elements d'una llista"
driveFolderBg: "Fons de la carpeta Disc"
wallpaperOverlay: "Superposició del fons de pantalla "
badge: "Insígnia "
messageBg: "Fons del xat"
accentDarken: "Accent (fosc)"
accentLighten: "Accent (clar)"
fgHighlighted: "Text ressaltat"
_sfx:
note: "Notes"
noteMy: "Nota (per mi)"
notification: "Notificacions"
antenna: "Antenes"
channel: "Notificacions dels canals"
reaction: "Quan se selecciona una reacció "
_soundSettings:
driveFile: "Fer servir un fitxer d'àudio del disc"
driveFileWarn: "Seleccionar un fitxer d'àudio del disc"
driveFileTypeWarn: "Fitxer no suportat "
driveFileTypeWarnDescription: "Seleccionar un fitxer d'àudio "
driveFileDurationWarn: "L'àudio és massa llarg"
driveFileDurationWarnDescription: "Els àudios molt llargs pot interrompre l'ús de Misskey. Vols continuar?"
_ago:
future: "Futur "
justNow: "Ara mateix"
secondsAgo: "Fa {n} segons"
minutesAgo: "Fa {n} minuts"
hoursAgo: "Fa {n} hores"
daysAgo: "Fa {n} dies"
weeksAgo: "Fa {n} setmanes"
monthsAgo: "Fa {n} mesos"
yearsAgo: "Fa {n} anys"
invalid: "Res"
_timeIn:
seconds: "En {n} segons"
minutes: "En {n} minuts"
hours: "En {n} hores"
days: "En {n} dies"
weeks: "En {n} setmanes"
months: "En {n} mesos"
years: "En {n} anys"
_time:
second: "Segon(s)"
minute: "Minut(s)"
hour: "Hor(a)(es)"
day: "Di(a)(es)"
_2fa:
alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor."
registerTOTP: "Registrar una aplicació autenticadora"
step1: "Primer instal·la una aplicació autenticadora (com {a} o {b}) al teu dispositiu."
step2: "Després escaneja el codi QR que es mostra en aquesta pantalla."
step2Click: "Fent clic en aquest codi QR et permetrà registrar l'autenticació de doble factor a la teva clau de seguretat o en l'aplicació d'autenticació del teu dispositiu."
step2Uri: "Escriu la següent URI si estàs fent servir una aplicació d'escriptori "
step3Title: "Escriu un codi d'autenticació"
step3: "Escriu el codi d'autenticació (token) que es mostra a la teva aplicació per finalitzar la configuració."
setupCompleted: "Configuració terminada"
step4: "D'ara endavant quan accedeixis se't demanarà el token que has introduït."
securityKeyNotSupported: "El teu navegador no suporta claus de seguretat"
removeKeyConfirm: "Esborrar la còpia de seguretat {name}?"
renewTOTPCancel: "No, gràcies"
_antennaSources:
all: "Totes les publicacions"
@ -1401,6 +1889,8 @@ _widgets:
chooseList: "Tria una llista"
_cw:
show: "Carregar més"
_poll:
deadlineTime: "Hor(a)(es)"
_visibility:
home: "Inici"
followers: "Seguidors"

View File

@ -1005,6 +1005,7 @@ resetPasswordConfirm: "Opravdu chcete resetovat heslo?"
sensitiveWords: "Citlivá slova"
sensitiveWordsDescription: "Viditelnost všech poznámek obsahujících některé z nakonfigurovaných slov bude automaticky nastavena na \"Domů\". Můžete jich uvést více tak, že je oddělíte pomocí řádků."
sensitiveWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz."
prohibitedWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz."
notesSearchNotAvailable: "Vyhledávání poznámek je nedostupné."
license: "Licence"
unfavoriteConfirm: "Opravdu chcete odstranit z oblíbených?"

View File

@ -1037,6 +1037,7 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
hiddenTags: "Ausgeblendete Hashtags"
hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden."
notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar."

View File

@ -1078,6 +1078,7 @@ resetPasswordConfirm: "Really reset your password?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
hiddenTags: "Hidden hashtags"
hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines."
notesSearchNotAvailable: "Note search is unavailable."
@ -2023,54 +2024,30 @@ _permissions:
"read:flash-likes": "View list of liked Plays"
"write:flash-likes": "Edit list of liked Plays"
"read:admin:abuse-user-reports": "View user reports"
"write:admin:delete-account": "Delete account"
"write:admin:delete-account": "Delete user account"
"write:admin:delete-all-files-of-a-user": "Delete all files of a user"
"read:admin:index-stats": "View information about database indexes"
"read:admin:table-stats": "View information about database tables"
"read:admin:user-ips": "View user IP address"
"read:admin:meta": "View instance metadata"
"write:admin:reset-password": "Reset user passwords"
"write:admin:resolve-abuse-user-report": "Resolve user reports"
"write:admin:send-email": "Send Email"
"write:admin:reset-password": "Reset user password"
"write:admin:send-email": "Send email"
"read:admin:server-info": "View server info"
"read:admin:show-moderation-log": "View moderation log"
"read:admin:show-user": "View user information"
"read:admin:show-users": "View users"
"read:admin:show-user": "View private user info"
"read:admin:show-users": "View private user info"
"write:admin:suspend-user": "Suspend user"
"write:admin:unset-user-avatar": "Remove avatar from user"
"write:admin:unset-user-banner": "Remove banner from user"
"write:admin:unset-user-avatar": "Remove user avatar"
"write:admin:unset-user-banner": "Remove user banner"
"write:admin:unsuspend-user": "Unsuspend user"
"write:admin:meta": "Edit instance metadata"
"write:admin:user-note": "Edit user note"
"write:admin:roles": "Edit roles"
"write:admin:meta": "Manage instance metadata"
"write:admin:user-note": "Manage moderation note"
"write:admin:roles": "Manage roles"
"read:admin:roles": "View roles"
"write:admin:relays": "Edit relays"
"write:admin:relays": "Manage relays"
"read:admin:relays": "View relays"
"write:admin:invite-codes": "Edit invite codes"
"write:admin:invite-codes": "Manage invite codes"
"read:admin:invite-codes": "View invite codes"
"write:admin:announcements": "Edit announcements"
"write:admin:announcements": "Manage announcements"
"read:admin:announcements": "View announcements"
"write:admin:avatar-decorations": "Edit avatar decorations"
"read:admin:avatar-decorations": "View avatar decorations"
"write:admin:federation": "Edit remote instance information"
"write:admin:account": "Edit users"
"read:admin:account": "View information about user"
"write:admin:emoji": "Edit emojis"
"read:admin:emoji": "View emojis"
"write:admin:queue": "Edit queue"
"read:admin:queue": "View queue"
"write:admin:promo": "Edit promo"
"write:admin:drive": "Edit user drive"
"read:admin:drive": "View user drive"
"read:admin:stream": "Using the Websocket API for Admin"
"write:admin:ad": "Edit ads"
"read:admin:ad": "View ads"
"write:invite-codes": "Create Invitation Code"
"read:invite-codes": "View Invitation Code"
"write:clip-favorite": "Edit clips and likes"
"read:clip-favorite": "View clips and likes"
"read:federation": "View information about remote instance"
"write:report-abuse": "Report abuse"
"write:admin:avatar-decorations": "Manage avatar decorations"
_auth:
shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?"

View File

@ -1041,6 +1041,7 @@ resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares."
prohibitedWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares."
hiddenTags: "Hashtags ocultos"
hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea."
notesSearchNotAvailable: "No se puede buscar una nota"

View File

@ -1038,6 +1038,7 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?"
sensitiveWords: "Kata sensitif"
sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru."
sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
hiddenTags: "Tagar tersembunyi"
hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris."
notesSearchNotAvailable: "Pencarian catatan tidak tersedia."

12
locales/index.d.ts vendored
View File

@ -4329,6 +4329,18 @@ export interface Locale extends ILocale {
* AND指定になり
*/
"sensitiveWordsDescription2": string;
/**
*
*/
"prohibitedWords": string;
/**
* 稿
*/
"prohibitedWordsDescription": string;
/**
* AND指定になり
*/
"prohibitedWordsDescription2": string;
/**
*
*/

View File

@ -103,7 +103,7 @@ defaultNoteVisibility: "Privacy predefinita delle note"
follow: "Segui"
followRequest: "Richiesta di follow"
followRequests: "Richieste di follow"
unfollow: "Interrompi following"
unfollow: "Smetti di seguire"
followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji"
renote: "Rinota"
@ -381,9 +381,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Abilita hCaptcha"
hcaptchaSiteKey: "Chiave del sito"
hcaptchaSecretKey: "Chiave segreta"
mcaptcha: "mCaptcha"
enableMcaptcha: "Abilita hCaptcha"
mcaptchaSiteKey: "Chiave del sito"
mcaptchaSecretKey: "Chiave segreta"
mcaptchaInstanceUrl: "URL della istanza mCaptcha"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Abilita reCAPTCHA"
recaptchaSiteKey: "Chiave del sito"
@ -631,6 +633,7 @@ medium: "Medio"
small: "Piccolo"
generateAccessToken: "Genera token di accesso"
permission: "Autorizzazioni "
adminPermission: "Privilegi amministrativi"
enableAll: "Abilita tutto"
disableAll: "Disabilita tutto"
tokenRequested: "Autorizza accesso al profilo"
@ -674,6 +677,7 @@ useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifi
other: "Ulteriori"
regenerateLoginToken: "Genera di nuovo un token di connessione"
regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate"
setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
fileIdOrUrl: "ID o URL del file"
behavior: "Comportamento"
@ -872,7 +876,7 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
unresolved: "Non risolto"
breakFollow: "Interrompi follow"
breakFollow: "Impedire di seguirmi"
breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?"
itsOn: "Abilitato"
itsOff: "Disabilitato"
@ -888,6 +892,8 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
classic: "Classico"
muteThread: "Silenzia conversazione"
unmuteThread: "Riattiva la conversazione"
followingVisibility: "Visibilità dei profili seguiti"
followersVisibility: "Visibilità dei profili che ti seguono"
continueThread: "Altre conversazioni"
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
incorrectPassword: "La password è errata."
@ -1039,6 +1045,7 @@ resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole esplicite"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
prohibitedWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
hiddenTags: "Hashtag nascosti"
hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga."
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
@ -1057,6 +1064,8 @@ limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
noteIdOrUrl: "ID della Nota o URL"
video: "Video"
videos: "Video"
audio: "Audio"
audioFiles: "Audio"
dataSaver: "Risparmia dati"
accountMigration: "Migrazione del profilo"
accountMoved: "Questo profilo ha migrato altrove:"
@ -1187,7 +1196,27 @@ remainingN: "Rimangono: {n}"
overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?"
seasonalScreenEffect: "Schermate in base alla stagione"
decorate: "Decora"
addMfmFunction: "Aggiungi decorazioni"
enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM"
bubbleGame: "Bubble Game"
sfx: "Effetti sonori"
soundWillBePlayed: "Verrà riprodotto il suono"
showReplay: "Vedi i replay"
replay: "Replay"
replaying: "Replay in corso"
ranking: "Classifica"
lastNDays: "Ultimi {n} giorni"
backToTitle: "Torna al titolo"
hemisphere: "Geolocalizzazione"
withSensitive: "Mostra le Note con allegati espliciti"
userSaysSomethingSensitive: "Note da {name} con allegati espliciti"
enableHorizontalSwipe: "Trascina per invertire i tab"
_bubbleGame:
howToPlay: "Come giocare"
_howToPlay:
section1: "Regola la posizione e rilascia l'oggetto nella casella."
section2: "Ottieni un punteggio, quando due oggetti dello stesso tipo si toccano e si trasformano in un oggetto diverso."
section3: "Se gli oggetti traboccano dalla scatola, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dalla scatola!"
_announcement:
forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1558,6 +1587,13 @@ _achievements:
_tutorialCompleted:
title: "Attestato di partecipazione al corso per principianti di Misskey"
description: "Ha completato il tutorial"
_bubbleGameExplodingHead:
title: "🤯"
description: "Estrai l'oggetto più grande dal Bubble Game"
_bubbleGameDoubleExplodingHead:
title: "Doppio 🤯"
description: "Due oggetti più grossi contemporaneamente nel Bubble Game"
flavor: "Ha le dimensioni di una bento-box 🤯 🤯"
_role:
new: "Nuovo ruolo"
edit: "Modifica ruolo"
@ -1648,6 +1684,7 @@ _emailUnavailable:
disposable: "Indirizzo email non utilizzabile"
mx: "Server email non corretto"
smtp: "Il server email non risponde"
banned: "Non puoi registrarti con questo indirizzo email"
_ffVisibility:
public: "Pubblica"
followers: "Mostra solo ai follower"
@ -1940,6 +1977,26 @@ _permissions:
"write:flash": "Modifica Play"
"read:flash-likes": "Visualizza lista di Play piaciuti"
"write:flash-likes": "Modifica lista di Play piaciuti"
"read:admin:abuse-user-reports": "Mostra i report dai profili utente"
"write:admin:delete-account": "Elimina l'account utente"
"write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente"
"read:admin:index-stats": "Visualizza informazioni sugli indici del database"
"read:admin:table-stats": "Visualizza informazioni sulle tabelle del database"
"read:admin:user-ips": "Visualizza indirizzi IP degli account"
"read:admin:meta": "Visualizza i metadati dell'istanza"
"write:admin:reset-password": "Ripristina la password dell'account utente"
"write:admin:resolve-abuse-user-report": "Risolvere le segnalazioni dagli account utente"
"write:admin:send-email": "Spedire email"
"read:admin:server-info": "Vedere le informazioni sul server"
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
"read:admin:show-user": "Vedere le informazioni private degli account utente"
"read:admin:show-users": "Vedere le informazioni private degli account utente"
"write:admin:suspend-user": "Sospendere i profili"
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
"write:admin:unsuspend-user": "Togliere la sospensione ai profili"
"write:admin:meta": "Modificare i metadati dell'istanza"
"write:admin:user-note": "Scrivere annotazioni di moderazione"
_auth:
shareAccessTitle: "Permessi dell'applicazione"
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"

View File

@ -1078,6 +1078,9 @@ resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
prohibitedWords: "禁止ワード"
prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。"
prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
hiddenTags: "非表示ハッシュタグ"
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
notesSearchNotAvailable: "ノート検索は利用できません。"

View File

@ -1043,6 +1043,7 @@ resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語"
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。"
prohibitedWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。"
hiddenTags: "見えてへんハッシュタグ"
hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。"
notesSearchNotAvailable: "なんかノート探せへん。"

View File

@ -40,7 +40,7 @@ favorites: "질겨찾기"
unfavorite: "질겨찾기서 어ᇝ애기"
favorited: "질겨찾기에 담앗십니다."
alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다."
cantFavorite: "질겨찾기에 몬 담십니다."
cantFavorite: "질겨찾기에 몬 담십니다."
pin: "프로필에 붙이기"
unpin: "프로필서 띠기"
copyContent: "내용 복사하기"
@ -124,6 +124,7 @@ reactions: "반엉"
reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, +’럴 누질라서 옇십니다."
rememberNoteVisibility: "공개 범위럴 기억하기"
attachCancel: "붙임 빼기"
deleteFile: "파일 뭉캐기"
markAsSensitive: "수ᇚ힘 설정"
unmarkAsSensitive: "수ᇚ힘 무루기"
enterFileName: "파일 이럼 서기"
@ -463,6 +464,8 @@ onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니
invitations: "초대하기"
invitationCode: "초대장"
checking: "학인하고 잇십니다"
tooShort: "억수로 짜립니다"
tooLong: "억수로 집니다"
passwordMatched: "맞십니다"
passwordNotMatched: "안 맞십니다"
signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
@ -571,7 +574,11 @@ userSilenced: "요 게정은... 수ᇚ혀 있십니다."
relays: "릴레이"
addRelay: "릴레이 옇기"
addedRelays: "옇은 릴레이"
deletedNote: "뭉캔 걸"
enableInfiniteScroll: "알아서 더 보기"
useCw: "내용 수ᇚ후기"
description: "설멩"
describeFile: "캡션 옇기"
author: "맨던 사람"
manage: "간리"
emailServer: "전자우펜 서버"
@ -600,6 +607,7 @@ renotesCount: "리노트한 수"
renotedCount: "리노트덴 수"
followingCount: "팔로우 수"
followersCount: "팔로워 수"
noteFavoritesCount: "질겨찾기한 노트 수"
clips: "클립 맨걸기"
clearCache: "캐시 비우기"
unlikeConfirm: "좋네예럴 무룹니꺼?"
@ -608,6 +616,7 @@ user: "사용자"
administration: "간리"
on: "킴"
off: "껌"
hide: "수ᇚ후기"
clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소."
searchByGoogle: "찾기"
tenMinutes: "십 분"
@ -626,9 +635,11 @@ role: "옉할"
noRole: "옉할이 없십니다"
thisPostMayBeAnnoyingCancel: "아이예"
likeOnly: "좋네예마"
myClips: "내 클립"
icon: "아바타"
replies: "답하기"
renotes: "리노트"
attach: "옇기"
_initialAccountSetting:
startTutorial: "길라잡이 하기"
_initialTutorial:
@ -641,9 +652,52 @@ _initialTutorial:
title: "길라잡이가 껕낫십니다!🎉"
_achievements:
_types:
_notes1:
description: "첫 노트럴 섯어예"
_notes10:
description: "노트럴 10번 섰어예"
_notes100:
description: "노트럴 100번 섰어예"
_notes500:
description: "노트럴 500번 섰어예"
_notes1000:
description: "노트럴 1,000번 섰어예"
_notes5000:
description: "노트럴 5,000번 섰어예"
_notes10000:
description: "노트럴 10,000번 섰어예"
_notes20000:
description: "노트럴 20,000번 섰어예"
_notes30000:
description: "노트럴 30,000번 섰어예"
_notes40000:
description: "노트럴 40,000번 섰어예"
_notes50000:
description: "노트럴 50,000번 섰어예"
_notes60000:
description: "노트럴 60,000번 섰어예"
_notes70000:
description: "노트럴 70,000번 섰어예"
_notes80000:
description: "노트럴 80,000번 섰어예"
_notes90000:
description: "노트럴 90,000번 섰어예"
_notes100000:
description: "노트럴 100,000번 섰어예"
_noteClipped1:
description: "첫 노트럴 클립햇어예"
_noteFavorited1:
description: "첫 노트럴 질겨찾기에 담앗어예"
_myNoteFavorited1:
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
_iLoveMisskey:
description: "“I ❤ #Misskey”럴 섰어예"
_postedAt0min0sec:
description: "0분 0초에 노트를 섰어예"
_tutorialCompleted:
description: "길라잡이럴 껕냇십니다"
_gallery:
my: "내 걸"
liked: "좋네예한 걸"
like: "좋네예!"
unlike: "좋네예 무루기"
@ -654,7 +708,12 @@ _serverDisconnectedBehavior:
reload: "알아서 새로곤침"
_channel:
removeBanner: "배너 뭉캐기"
usersCount: "{n}명 참여"
notesCount: "노트 {n}개"
_menuDisplay:
hide: "수ᇚ후기"
_theme:
description: "설멩"
keys:
mention: "멘션"
_sfx:
@ -663,6 +722,9 @@ _sfx:
_2fa:
step3Title: "학인 기호럴 서기"
renewTOTPCancel: "뎃어예"
_permissions:
"read:favorites": "질겨찾기 보기"
"write:favorites": "질겨찾기 곤치기"
_widgets:
profile: "프로필"
instanceInfo: "서버 정보"
@ -674,7 +736,10 @@ _widgets:
_userList:
chooseList: "리스트 개리기"
_cw:
hide: "수ᇚ후기"
show: "더 볼래예"
chars: "걸자 {count}개"
files: "파일 {count}개"
_visibility:
home: "덜머리"
followers: "팔로워"
@ -682,6 +747,7 @@ _profile:
name: "이럼"
username: "사용자 이럼"
_exportOrImport:
favoritedNotes: "질겨찾기한 노트"
clips: "클립 맨걸기"
followingList: "팔로잉"
muteList: "수ᇚ후기"
@ -692,16 +758,20 @@ _charts:
_timelines:
home: "덜머리"
_play:
my: "내 플레이"
script: "스크립트"
summary: "설멩"
_pages:
like: "좋네예"
unlike: "좋네예 무루기"
my: "내 페이지"
blocks:
image: "이미지"
_note:
id: "노트 아이디"
_notification:
youWereFollowed: "새 팔로워가 잇십니다"
newNote: "새 걸"
_types:
follow: "팔로잉"
mention: "멘션"

View File

@ -279,7 +279,7 @@ uploadFromUrl: "URL 업로드"
uploadFromUrlDescription: "업로드하려는 파일의 URL"
uploadFromUrlRequested: "업로드를 요청했습니다"
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
explore: "발견하기"
explore: "둘러보기"
messageRead: "읽음"
noMoreHistory: "이것보다 과거의 기록이 없습니다"
startMessaging: "대화 시작하기"
@ -1022,7 +1022,7 @@ internalServerError: "내부 서버 오류"
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기"
exploreOtherServers: "다른 서버 기"
letsLookAtTimeline: "타임라인 구경하기"
disableFederationConfirm: "정말로 연합을 끄시겠습니까?"
disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아닙니다. 대부분의 경우 연합을 비활성화할 필요가 없습니다."
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어"
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
hiddenTags: "숨긴 해시태그"
hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다."
notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다."
@ -1797,7 +1798,7 @@ _instanceMute:
title: "지정한 서버의 노트를 숨깁니다."
heading: "뮤트할 서버"
_theme:
explore: "테마 찾아보기"
explore: "테마 둘러보기"
install: "테마 설치"
manage: "테마 관리"
code: "테마 코드"
@ -2022,7 +2023,7 @@ _permissions:
"write:report-abuse": "위반 내용 신고하기"
_auth:
shareAccessTitle: "어플리케이션의 접근 허가"
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
shareAccess: "{name}’에서 계정에 접근하는 것을 허용하시겠습니까?"
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?"
permission: "{name}에서 다음 권한을 요청하였습니다"
permissionAsk: "이 앱은 다음의 권한을 요청합니다"

View File

@ -1015,6 +1015,7 @@ resetPasswordConfirm: "Сбросить пароль?"
sensitiveWords: "Чувствительные слова"
sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк."
sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
notesSearchNotAvailable: "Поиск заметок недоступен"
license: "Лицензия"
unfavoriteConfirm: "Удалить избранное?"

View File

@ -1041,6 +1041,7 @@ resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุ
sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
hiddenTags: "แฮชแท็กที่ซ่อนอยู่"
hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน"

View File

@ -1041,6 +1041,7 @@ resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
hiddenTags: "隐藏标签"
hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。"
notesSearchNotAvailable: "帖子检索不可用"

View File

@ -1041,6 +1041,7 @@ resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞"
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
sensitiveWordsDescription2: "空格代表「以及」AND斜線包圍關鍵字代表使用正規表達式。"
prohibitedWordsDescription2: "空格代表「以及」AND斜線包圍關鍵字代表使用正規表達式。"
hiddenTags: "隱藏標籤"
hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。"
notesSearchNotAvailable: "無法使用搜尋貼文功能。"

View File

@ -1,12 +1,12 @@
{
"name": "sharkey",
"version": "2024.2.0-beta.9",
"version": "2024.2.0-beta.11",
"codename": "shonk",
"repository": {
"type": "git",
"url": "https://git.joinsharkey.org/Sharkey/Sharkey.git"
},
"packageManager": "pnpm@8.12.1",
"packageManager": "pnpm@8.15.1",
"workspaces": [
"packages/frontend",
"packages/backend",

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class prohibitedWords1707429690000 {
name = 'prohibitedWords1707429690000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`);
}
}

View File

@ -84,7 +84,7 @@
"@nestjs/testing": "10.2.10",
"@peertube/http-signature": "1.7.0",
"@transfem-org/sfm-js": "0.24.4",
"@simplewebauthn/server": "9.0.1",
"@simplewebauthn/server": "9.0.2",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63",
@ -98,12 +98,12 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.1.5",
"bullmq": "5.1.9",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"cbor": "9.0.2",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "3.5.3",
"chokidar": "3.6.0",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
@ -205,7 +205,7 @@
"@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.11.10",
"@types/node": "20.11.17",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4",

View File

@ -408,7 +408,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
*/
@bindThis
public checkDuplicate(name: string): Promise<boolean> {
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
return this.emojisRepository.exists({ where: { name, host: IsNull() } });
}
@bindThis

View File

@ -40,6 +40,8 @@ export class EmailService {
public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true);
if (!meta.enableEmail) return;
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`;

View File

@ -163,7 +163,7 @@ export class HashtagService {
const instance = await this.metaService.fetch();
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return;
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();

View File

@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser } from '@/models/User.js';
import type { UsersRepository } from '@/models/_.js';
import { MemorySingleCache } from '@/misc/cache.js';
@ -27,6 +27,14 @@ export class InstanceActorService {
this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
}
@bindThis
public async realLocalUsersPresent(): Promise<boolean> {
return await this.usersRepository.existsBy({
host: IsNull(),
username: Not(ACTOR_USERNAME),
});
}
@bindThis
public async getInstanceActor(): Promise<MiLocalUser> {
const cached = this.cache.get();

View File

@ -419,6 +419,10 @@ export class MfmService {
},
text: (node) => {
if (!node.props.text.match(/[\r\n]/)) {
return doc.createTextNode(node.props.text);
}
const el = doc.createElement('span');
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));

View File

@ -153,6 +153,8 @@ type Option = {
export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
public static ContainsProhibitedWordsError = class extends Error {};
constructor(
@Inject(DI.config)
private config: Config,
@ -257,7 +259,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@ -429,13 +431,19 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
}
}
if (!user.host) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
throw new NoteCreateService.ContainsProhibitedWordsError();
}
}
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
@ -795,7 +803,7 @@ export class NoteCreateService implements OnApplicationShutdown {
});
// 通知
if (data.reply.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
@ -830,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Notify
if (data.renote.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.renote.userId,
threadId: data.renote.threadId ?? data.renote.id,
@ -1057,7 +1065,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,

View File

@ -145,6 +145,8 @@ type Option = {
export class NoteEditService implements OnApplicationShutdown {
#shutdownController = new AbortController();
public static ContainsProhibitedWordsError = class extends Error {};
constructor(
@Inject(DI.config)
private config: Config,
@ -266,7 +268,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@ -612,7 +614,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.reply) {
// 通知
if (data.reply.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
@ -647,7 +649,7 @@ export class NoteEditService implements OnApplicationShutdown {
// Notify
if (data.renote.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.renote.userId,
threadId: data.renote.threadId ?? data.renote.id,
@ -751,7 +753,7 @@ export class NoteEditService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,

View File

@ -49,7 +49,7 @@ export class NoteReadService implements OnApplicationShutdown {
//#endregion
// スレッドミュート
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
@ -70,7 +70,7 @@ export class NoteReadService implements OnApplicationShutdown {
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
if (!exist) return;

View File

@ -248,7 +248,7 @@ export class ReactionService {
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: note.userId,
threadId: note.threadId ?? note.id,

View File

@ -17,6 +17,7 @@ import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
@ -38,6 +39,7 @@ export class SignupService {
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
private usersChart: UsersChart,
) {
}
@ -75,16 +77,16 @@ export class SignupService {
const secret = generateUserToken();
// Check username duplication
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME');
}
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());

View File

@ -144,7 +144,7 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,
@ -156,7 +156,7 @@ export class UserFollowingService implements OnModuleInit {
// フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const isFollowed = await this.followingsRepository.exist({
const isFollowed = await this.followingsRepository.exists({
where: {
followerId: followee.id,
followeeId: follower.id,
@ -170,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
if (followee.isLocked && !autoAccept) {
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
follower,
(oldSrc, newSrc) => this.followingsRepository.exist({
(oldSrc, newSrc) => this.followingsRepository.exists({
where: {
followeeId: followee.id,
followerId: newSrc.id,
@ -233,7 +233,7 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id);
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followeeId: followee.id,
followerId: follower.id,
@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
}
}
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followeeId: followee.id,
followerId: follower.id,

View File

@ -43,13 +43,13 @@ export class UtilityService {
}
@bindThis
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
if (sensitiveWords.length === 0) return false;
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
if (keyWords.length === 0) return false;
if (text === '') return false;
const regexpregexp = /^\/(.+)\/(.*)$/;
const matched = sensitiveWords.some(filter => {
const matched = keyWords.some(filter => {
// represents RegExp
const regexp = filter.match(regexpregexp);
// This should never happen due to input sanitisation.

View File

@ -629,7 +629,7 @@ export class ApInboxService {
return 'skip: follower not found';
}
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: actor.id,
@ -686,14 +686,14 @@ export class ApInboxService {
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
}
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followerId: actor.id,
followeeId: followee.id,
},
});
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: actor.id,
followeeId: followee.id,

View File

@ -25,8 +25,21 @@ export class ApMfmService {
}
@bindThis
public getNoteHtml(note: MiNote): string | null {
if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), note.mentionedRemoteUsers ? JSON.parse(note.mentionedRemoteUsers) : []);
public getNoteHtml(note: MiNote, apAppend?: string) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? '');
const parsed = mfm.parse(srcMfm);
if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true;
}
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers));
return {
content,
noMisskeyContent,
};
}
}

View File

@ -345,7 +345,7 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
if (inReplyToUserExist) {
if (inReplyToNote.uri) {
@ -409,17 +409,15 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
let apText = text;
let apAppend = '';
if (quote) {
apText += `\n\nRE: ${quote}`;
apAppend += `\n\nRE: ${quote}`;
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
text: apText,
}));
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
@ -432,9 +430,6 @@ export class ApRendererService {
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',
@ -452,11 +447,13 @@ export class ApRendererService {
attributedTo,
summary: summary ?? undefined,
content: content ?? undefined,
_misskey_content: text,
source: {
content: text,
mediaType: 'text/x.misskeymarkdown',
},
...(noMisskeyContent ? {} : {
_misskey_content: text,
source: {
content: text,
mediaType: 'text/x.misskeymarkdown',
},
}),
_misskey_quote: quote,
quoteUrl: quote,
quoteUri: quote,
@ -639,7 +636,7 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
if (inReplyToUserExist) {
if (inReplyToNote.uri) {
@ -796,6 +793,7 @@ export class ApRendererService {
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{
Key: 'sec:Key',
// as non-standards
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
sensitive: 'as:sensitive',

View File

@ -225,23 +225,42 @@ export class ApPersonService implements OnModuleInit {
return null;
}
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>>> {
if (user == null) throw new Error('failed to create user: user is null');
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
if (img == null) return null;
if (user == null) throw new Error('failed to create user: user is null');
// if we have an explicitly missing image, return an
// explicitly-null set of values
if ((img == null) || (typeof img === 'object' && img.url == null)) {
return { id: null, url: null, blurhash: null };
}
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
/*
we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old
values, we should keep those. The exception is if the remote has
actually removed the images: in that case, the block above
returns the special {id:null}&c value, and we return those
*/
return {
avatarId: avatar?.id ?? null,
bannerId: banner?.id ?? null,
backgroundId: background?.id ?? null,
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
avatarBlurhash: avatar?.blurhash ?? null,
bannerBlurhash: banner?.blurhash ?? null,
backgroundBlurhash: background?.blurhash ?? null
...( avatar ? {
avatarId: avatar.id,
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
avatarBlurhash: avatar.blurhash,
} : {}),
...( banner ? {
bannerId: banner.id,
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
bannerBlurhash: banner.blurhash,
} : {}),
...( background ? {
backgroundId: background.id,
backgroundUrl: background.url ? this.driveFileEntityService.getPublicUrl(background) : null,
backgroundBlurhash: background.blurhash,
} : {}),
};
}

View File

@ -51,14 +51,14 @@ export class ChannelEntityService {
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
const isFollowing = meId ? await this.channelFollowingsRepository.exists({
where: {
followerId: meId,
followeeId: channel.id,
},
}) : false;
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
const isFavorited = meId ? await this.channelFavoritesRepository.exists({
where: {
userId: meId,
channelId: channel.id,

View File

@ -46,7 +46,7 @@ export class ClipEntityService {
description: clip.description,
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
});
}

View File

@ -31,6 +31,7 @@ export class EmojiEntityService {
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
};

View File

@ -47,7 +47,7 @@ export class FlashEntityService {
summary: flash.summary,
script: flash.script,
likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
});
}

View File

@ -53,7 +53,7 @@ export class GalleryPostEntityService {
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined,
});
}

View File

@ -114,7 +114,7 @@ export class NoteEntityService implements OnModuleInit {
hide = false;
} else {
if (packedNote.renote) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.renote.userId,
followerId: meId,
@ -124,7 +124,7 @@ export class NoteEntityService implements OnModuleInit {
hide = !isFollowing;
} else {
// フォロワーかどうか
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,

View File

@ -104,7 +104,7 @@ export class PageEntityService {
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
});
}

View File

@ -153,43 +153,43 @@ export class UserEntityService implements OnModuleInit {
followerId: me,
followeeId: target,
}),
this.followingsRepository.exist({
this.followingsRepository.exists({
where: {
followerId: target,
followeeId: me,
},
}),
this.followRequestsRepository.exist({
this.followRequestsRepository.exists({
where: {
followerId: me,
followeeId: target,
},
}),
this.followRequestsRepository.exist({
this.followRequestsRepository.exists({
where: {
followerId: target,
followeeId: me,
},
}),
this.blockingsRepository.exist({
this.blockingsRepository.exists({
where: {
blockerId: me,
blockeeId: target,
},
}),
this.blockingsRepository.exist({
this.blockingsRepository.exists({
where: {
blockerId: target,
blockeeId: me,
},
}),
this.mutingsRepository.exist({
this.mutingsRepository.exists({
where: {
muterId: me,
muteeId: target,
},
}),
this.renoteMutingsRepository.exist({
this.renoteMutingsRepository.exists({
where: {
muterId: me,
muteeId: target,
@ -216,7 +216,7 @@ export class UserEntityService implements OnModuleInit {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({
where: {
antennaId: In(myAntennas.map(x => x.id)),
read: false,

View File

@ -76,6 +76,11 @@ export class MiMeta {
})
public sensitiveWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public prohibitedWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})

View File

@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
type: 'string',
optional: false, nullable: false,
},
localOnly: {
type: 'boolean',
optional: true, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,

View File

@ -176,12 +176,12 @@ export class SignupApiService {
}
if (instance.emailRequiredForSignup) {
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new FastifyReplyError(400, 'USED_USERNAME');
}

View File

@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { Packed } from '@/misc/json-schema.js';
@ -46,13 +47,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const noUsers = (await this.usersRepository.countBy({
host: IsNull(),
})) === 0;
if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
const realUsers = await this.instanceActorService.realLocalUsersPresent();
if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
const { account, secret } = await this.signupService.signup({
username: ps.username,

View File

@ -160,6 +160,13 @@ export const meta = {
type: 'string',
},
},
prohibitedWords: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@ -549,6 +556,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,

View File

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw e;
});
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } });
if (exist) {
throw new ApiError(meta.errors.alreadyPromoted);

View File

@ -41,6 +41,11 @@ export const paramDef = {
type: 'string',
},
},
prohibitedWords: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@ -185,6 +190,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}
if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
}
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {

View File

@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const accessToken = secureRndstr(32);
// Fetch exist access token
const exist = await this.accessTokensRepository.exist({
const exist = await this.accessTokensRepository.exists({
where: {
appId: session.appId,
userId: me.id,

View File

@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already blocking
const exist = await this.blockingsRepository.exist({
const exist = await this.blockingsRepository.exists({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,

View File

@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check not blocking
const exist = await this.blockingsRepository.exist({
const exist = await this.blockingsRepository.exists({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,

View File

@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchClip);
}
const exist = await this.clipFavoritesRepository.exist({
const exist = await this.clipFavoritesRepository.exists({
where: {
clipId: clip.id,
userId: me.id,

View File

@ -38,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const exist = await this.driveFilesRepository.exist({
const exist = await this.driveFilesRepository.exists({
where: {
md5: ps.md5,
userId: me.id,

View File

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.flashLikesRepository.exist({
const exist = await this.flashLikesRepository.exists({
where: {
flashId: flash.id,
userId: me.id,

View File

@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already following
const exist = await this.followingsRepository.exist({
const exist = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,

View File

@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check not following
const exist = await this.followingsRepository.exist({
const exist = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,

View File

@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.galleryLikesRepository.exist({
const exist = await this.galleryLikesRepository.exists({
where: {
postId: post.id,
userId: me.id,

View File

@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private downloadService: DownloadService,
) {
super(meta, paramDef, async (ps, me) => {
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
const userExist = await this.usersRepository.exists({ where: { id: me.id } });
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file === null) throw new ApiError(meta.errors.noSuchFile);

View File

@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.tokenId) {
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } });
if (tokenExist) {
await this.accessTokensRepository.delete({
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
} else if (ps.token) {
const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } });
const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } });
if (tokenExist) {
await this.accessTokensRepository.delete({

View File

@ -6,10 +6,11 @@
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/_.js';
import type { AdsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@ -337,14 +338,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
@ -427,9 +426,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: (await this.usersRepository.countBy({
host: IsNull(),
})) === 0,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
} : {}),
};

View File

@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already muting
const exist = await this.mutingsRepository.exist({
const exist = await this.mutingsRepository.exists({
where: {
muterId: muter.id,
muteeId: mutee.id,

View File

@ -17,6 +17,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -117,6 +119,12 @@ export const meta = {
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
},
} as const;
@ -271,7 +279,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
@ -319,7 +327,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,
@ -351,31 +359,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// 投稿を作成
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: ps.text ?? undefined,
reply,
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
});
try {
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: ps.text ?? undefined,
reply,
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
});
return {
createdNote: await this.noteEntityService.pack(note, me),
};
return {
createdNote: await this.noteEntityService.pack(note, me),
};
} catch (e) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof NoteCreateService.ContainsProhibitedWordsError) {
throw new ApiError(meta.errors.containsProhibitedWords);
}
throw e;
}
});
}
}

View File

@ -311,7 +311,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
@ -349,7 +349,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,

View File

@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// if already favorited
const exist = await this.noteFavoritesRepository.exist({
const exist = await this.noteFavoritesRepository.exists({
where: {
noteId: note.id,
userId: me.id,

View File

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.pageLikesRepository.exist({
const exist = await this.pageLikesRepository.exists({
where: {
pageId: page.id,
userId: me.id,

View File

@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
const exist = await this.promoReadsRepository.exist({
const exist = await this.promoReadsRepository.exists({
where: {
noteId: note.id,
userId: me.id,

View File

@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,

View File

@ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,

View File

@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const listExist = await this.userListsRepository.exist({
const listExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,
@ -121,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (currentUser.id !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: currentUser.id,
blockeeId: me.id,
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const exist = await this.userListMembershipsRepository.exist({
const exist = await this.userListMembershipsRepository.exists({
where: {
userListId: userList.id,
userId: currentUser.id,

View File

@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const userListExist = await this.userListsRepository.exist({
const userListExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchList);
}
const exist = await this.userListFavoritesRepository.exist({
const exist = await this.userListFavoritesRepository.exists({
where: {
userId: me.id,
userListId: ps.listId,

View File

@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (user.id !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: user.id,
blockeeId: me.id,
@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const exist = await this.userListMembershipsRepository.exist({
const exist = await this.userListMembershipsRepository.exists({
where: {
userListId: userList.id,
userId: user.id,

View File

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userListId: ps.listId,
});
if (me !== null) {
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
additionalProperties.isLiked = await this.userListFavoritesRepository.exists({
where: {
userId: me.id,
userListId: ps.listId,

View File

@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListFavoritesRepository: UserListFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const userListExist = await this.userListsRepository.exist({
const userListExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,

View File

@ -43,7 +43,7 @@ class UserListChannel extends Channel {
this.withRenotes = params.withRenotes ?? true;
// Check existence and owner
const listExist = await this.userListsRepository.exist({
const listExist = await this.userListsRepository.exists({
where: {
id: this.listId,
userId: this.user!.id,

View File

@ -16,12 +16,14 @@ describe('Note', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let tom: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
tom = await signup({ username: 'tom', host: 'example.com' });
}, 1000 * 60 * 2);
test('投稿できる', async () => {
@ -607,6 +609,77 @@ describe('Note', () => {
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'test',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note1.status, 400);
assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'/Test/i',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'Test hoge',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
text: 'hogeTesthuge',
}, alice);
assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含んでいてもリモートノートはエラーにならない', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'test',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, tom);
assert.strictEqual(note1.status, 200);
});
});
describe('notes/delete', () => {

View File

@ -0,0 +1,44 @@
import * as assert from 'assert';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { MiNote } from '@/models/Note.js';
describe('ApMfmService', () => {
let apMfmService: ApMfmService;
beforeAll(async () => {
const app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule],
}).compile();
apMfmService = app.get<ApMfmService>(ApMfmService);
});
describe('getNoteHtml', () => {
test('Do not provide _misskey_content for simple text', () => {
const note: MiNote = {
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
mentionedRemoteUsers: '[]',
} as any;
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
assert.equal(content, '<p>テキスト <a href="http://misskey.local/tags/タグ" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
});
test('Provide _misskey_content for MFM', () => {
const note: MiNote = {
text: '$[tada foo]',
mentionedRemoteUsers: '[]',
} as any;
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
assert.equal(content, '<p><i>foo</i></p>', 'content');
});
});
});

View File

@ -33,6 +33,12 @@ describe('MfmService', () => {
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('Do not generate unnecessary span', () => {
const input = 'foo $[tada bar]';
const output = '<p>foo <i>bar</i></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
});
describe('fromHtml', () => {

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { type SharedOptions, rest } from 'msw';
import { type SharedOptions, http, HttpResponse } from 'msw';
export const onUnhandledRequest = ((req, print) => {
if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
@ -13,19 +13,31 @@ export const onUnhandledRequest = ((req, print) => {
}) satisfies SharedOptions['onUnhandledRequest'];
export const commonHandlers = [
rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
},
});
}),
rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
},
});
}),
rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/svg+xml',
},
});
}),
];

View File

@ -30,7 +30,7 @@
"@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.3",
"@vue/compiler-sfc": "3.4.15",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
"astring": "1.8.6",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
@ -72,7 +72,7 @@
"typescript": "5.3.3",
"uuid": "9.0.1",
"v-code-diff": "1.7.2",
"vite": "5.0.12",
"vite": "5.1.0",
"vue": "3.4.15",
"vuedraggable": "next"
},
@ -102,7 +102,7 @@
"@types/estree": "1.0.5",
"@types/matter-js": "0.19.6",
"@types/micromatch": "4.0.6",
"@types/node": "20.11.10",
"@types/node": "20.11.17",
"@types/punycode": "2.1.3",
"@types/sanitize-html": "2.9.5",
"@types/throttle-debounce": "5.0.2",
@ -115,7 +115,7 @@
"@vue/runtime-core": "3.4.15",
"acorn": "8.11.3",
"cross-env": "7.0.3",
"cypress": "13.6.3",
"cypress": "13.6.4",
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-vue": "9.20.1",
@ -123,10 +123,10 @@
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.5",
"msw": "2.1.2",
"msw-storybook-addon": "1.10.0",
"msw": "2.1.7",
"msw-storybook-addon": "2.0.0-beta.1",
"nodemon": "3.0.3",
"prettier": "3.2.4",
"prettier": "3.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.3",

View File

@ -60,12 +60,6 @@ export async function common(createVue: () => App<Element>) {
});
}
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
let isClientUpdated = false;
//#region クライアントが更新されたかチェック
@ -293,5 +287,10 @@ function removeSplash() {
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
// transitionendイベントが発火しない場合があるため
window.setTimeout(() => {
splash.remove();
}, 1000);
}
}

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
action('POST /api/admin/resolve-abuse-user-report')(await req.json());
return res(ctx.json({}));
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
return HttpResponse.json({});
}),
],
},

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/report-abuse', async (req, res, ctx) => {
action('POST /api/users/report-abuse')(await req.json());
return res(ctx.json({}));
http.post('/api/users/report-abuse', async ({ request }) => {
action('POST /api/users/report-abuse')(await request.json());
return HttpResponse.json({});
}),
],
},

View File

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAchievements from './MkAchievements.vue';
@ -39,8 +39,8 @@ export const Empty = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json([]));
http.post('/api/users/achievements', () => {
return HttpResponse.json([]);
}),
],
},
@ -52,8 +52,8 @@ export const All = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
http.post('/api/users/achievements', () => {
return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
}),
],
},

View File

@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAutocomplete from './MkAutocomplete.vue';
@ -99,11 +99,11 @@ export const User = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/search-by-username-and-host', () => {
return HttpResponse.json([
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
]));
]);
}),
],
},
@ -132,12 +132,12 @@ export const Hashtag = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/hashtags/search', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/hashtags/search', () => {
return HttpResponse.json([
'気象警報注意報',
'気象警報',
'気象情報',
]));
]);
}),
],
},

View File

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAvatars from './MkAvatars.vue';
@ -38,12 +38,12 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/show', () => {
return HttpResponse.json([
userDetailed('17'),
userDetailed('20'),
userDetailed('18'),
]));
]);
}),
],
},

View File

@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable vue/no-v-html -->
<template>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { bundledLanguagesInfo } from 'shiki';
import type { BuiltinLanguage } from 'shiki';
import { getHighlighter } from '@/scripts/code-highlighter.js';
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
code: string;
@ -21,11 +22,23 @@ const props = defineProps<{
}>();
const highlighter = await getHighlighter();
const darkMode = defaultStore.reactiveState.darkMode;
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
const [lightThemeName, darkThemeName] = await Promise.all([
getTheme('light', true),
getTheme('dark', true),
]);
const html = computed(() => highlighter.codeToHtml(props.code, {
lang: codeLang.value,
theme: 'dark-plus',
themes: {
fallback: 'dark-plus',
light: lightThemeName,
dark: darkThemeName,
},
defaultColor: false,
cssVariablePrefix: '--shiki-',
}));
async function fetchLanguage(to: string): Promise<void> {
@ -79,6 +92,16 @@ watch(() => props.lang, (to) => {
margin: .5em 0;
overflow: auto;
border-radius: var(--radius-sm);
border: 1px solid var(--divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
& span {
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
}
& pre,
& code {
@ -86,6 +109,26 @@ watch(() => props.lang, (to) => {
}
}
.light.codeBlockRoot :global(.shiki) {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
& span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
}
}
.dark.codeBlockRoot :global(.shiki) {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
& span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
}
.codeBlockRoot.codeEditor {
min-width: 100%;
height: 100%;
@ -94,6 +137,7 @@ watch(() => props.lang, (to) => {
padding: 12px;
margin: 0;
border-radius: var(--radius-sm);
border: none;
min-height: 130px;
pointer-events: none;
min-width: calc(100% - 24px);
@ -105,6 +149,11 @@ watch(() => props.lang, (to) => {
text-rendering: inherit;
text-transform: inherit;
white-space: pre;
& span {
display: inline-block;
min-height: 1em;
}
}
}
</style>

View File

@ -53,7 +53,6 @@ function copy() {
}
.codeBlockCopyButton {
color: #D4D4D4;
position: absolute;
top: 8px;
right: 8px;
@ -67,8 +66,7 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
background: var(--bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@ -93,8 +91,8 @@ function copy() {
border-radius: var(--radius-sm);
padding: 24px;
margin-top: 4px;
color: #D4D4D4;
background: #1E1E1E;
color: var(--fg);
background: var(--bg);
}
.codePlaceholderContainer {

View File

@ -196,10 +196,11 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
caret-color: rgb(225, 228, 232);
caret-color: var(--fg);
background-color: transparent;
border: 0;
border-radius: var(--radius-sm);
box-sizing: border-box;
outline: 0;
min-width: calc(100% - 24px);
height: 100%;
@ -212,6 +213,6 @@ watch(v, newValue => {
}
.textarea::selection {
color: #fff;
color: var(--bg);
}
</style>

View File

@ -18,8 +18,7 @@ const props = defineProps<{
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
background: var(--bg);
padding: .1em;
border-radius: .3em;
}

View File

@ -63,18 +63,25 @@ const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
const croppedImage = await cropper?.getCropperImage();
const croppedSection = await cropper?.getCropperSelection();
// ()
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => {
if (!blob) return;
const formData = new FormData();
formData.append('file', blob);
formData.append('name', `cropped_${props.file.name}`);
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
formData.append('comment', props.file.comment ?? 'null');
if (props.file.comment) { formData.append('comment', props.file.comment);}
formData.append('i', $i!.token);
if (props.uploadFolder || props.uploadFolder === null) {
formData.append('folderId', props.uploadFolder ?? 'null');
} else if (defaultStore.state.uploadFolder) {
if (props.uploadFolder) {
formData.append('folderId', props.uploadFolder);
} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}

View File

@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
const props = withDefaults(defineProps<{
showPinned?: boolean;
@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean;
asWindow?: boolean;
asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note;
}>(), {
showPinned: true,
});
@ -340,7 +342,7 @@ watch(q, () => {
});
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
}
function focus() {

View File

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:showPinned="showPinned"
:pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker"
:targetNote="targetNote"
:asDrawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
showPinned?: boolean;
pinnedEmojis?: string[],
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
choseAndClose?: boolean;
}>(), {
manualShowing: null,

View File

@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:front="true"
@closed="emit('closed')"
>
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -26,6 +27,7 @@ withDefaults(defineProps<{
src?: HTMLElement;
showPinned?: boolean;
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note
}>(), {
showPinned: true,
});

View File

@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</Transition>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import { defaultStore } from '@/store.js';
import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
const rootEl = shallowRef<HTMLDivElement>();
@ -49,16 +49,16 @@ const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontal
// //
//
const MIN_SWIPE_DISTANCE = 50;
const MIN_SWIPE_DISTANCE = 20;
//
const SWIPE_DISTANCE_THRESHOLD = 125;
const SWIPE_DISTANCE_THRESHOLD = 70;
// Y
const SWIPE_ABORT_Y_THRESHOLD = 75;
//
const MAX_SWIPE_DISTANCE = 150;
const MAX_SWIPE_DISTANCE = 120;
// //
@ -68,7 +68,6 @@ let startScreenY: number | null = null;
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
const pullDistance = ref(0);
const isSwiping = ref(false);
const isSwipingForClass = ref(false);
let swipeAborted = false;
@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) {
if (event.touches.length !== 1) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
startScreenX = event.touches[0].screenX;
startScreenY = event.touches[0].screenY;
}
@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) {
if (swipeAborted) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
let distanceX = event.touches[0].screenX - startScreenX;
let distanceY = event.touches[0].screenY - startScreenY;
@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) {
if (!isSwiping.value) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
const distance = event.changedTouches[0].screenX - startScreenX;
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) {
}, 400);
}
/** 横スワイプに関与する可能性のある要素を調べる */
function hasSomethingToDoWithXSwipe(el: HTMLElement) {
if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true;
if (el.isContentEditable) return true;
if (el.scrollWidth > el.clientWidth) return true;
const style = window.getComputedStyle(el);
if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true;
if (['scroll', 'auto'].includes(style.overflowX)) return true;
if (style.touchAction === 'pan-x') return true;
if (el.parentElement && el.parentElement !== rootEl.value) {
return hasSomethingToDoWithXSwipe(el.parentElement);
} else {
return false;
}
}
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
watch(tabModel, (newTab, oldTab) => {
@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
<style lang="scss" module>
.transitionRoot {
touch-action: pan-y pinch-zoom;
display: grid;
grid-template-columns: 100%;
overflow: clip;

View File

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkInviteCode from './MkInviteCode.vue';
@ -39,8 +39,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json(userDetailed(req.params.userId as string)));
http.post('/api/users/show', ({ params }) => {
return HttpResponse.json(userDetailed(params.userId as string));
}),
],
},

View File

@ -119,6 +119,7 @@ function close() {
margin-top: 12px;
font-size: 0.8em;
line-height: 1.5em;
text-align: center;
}
> .indicatorWithValue {

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