merge: latest changes
This commit is contained in:
commit
85355813ad
@ -24,6 +24,8 @@
|
||||
- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
|
||||
* すべてのリモートユーザーのリアクション一覧を見えないようにします
|
||||
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
|
||||
- Fix: 特定のキーワードを含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207
|
||||
* デフォルトは空欄なので適用前と同等の動作になります
|
||||
|
||||
### Client
|
||||
- Feat: 新しいゲームを追加
|
||||
@ -74,6 +76,7 @@
|
||||
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
||||
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
|
||||
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
|
@ -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([]);
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
@ -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."
|
||||
@ -1518,12 +1521,82 @@ _achievements:
|
||||
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 "
|
||||
@ -1545,35 +1618,259 @@ _role:
|
||||
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"
|
||||
@ -1592,6 +1889,8 @@ _widgets:
|
||||
chooseList: "Tria una llista"
|
||||
_cw:
|
||||
show: "Carregar més"
|
||||
_poll:
|
||||
deadlineTime: "Hor(a)(es)"
|
||||
_visibility:
|
||||
home: "Inici"
|
||||
followers: "Seguidors"
|
||||
|
@ -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?"
|
||||
|
@ -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."
|
||||
|
@ -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?"
|
||||
|
@ -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"
|
||||
|
@ -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
12
locales/index.d.ts
vendored
@ -4329,6 +4329,18 @@ export interface Locale extends ILocale {
|
||||
* スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。
|
||||
*/
|
||||
"sensitiveWordsDescription2": string;
|
||||
/**
|
||||
* 禁止ワード
|
||||
*/
|
||||
"prohibitedWords": string;
|
||||
/**
|
||||
* 設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。
|
||||
*/
|
||||
"prohibitedWordsDescription": string;
|
||||
/**
|
||||
* スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。
|
||||
*/
|
||||
"prohibitedWordsDescription2": string;
|
||||
/**
|
||||
* 非表示ハッシュタグ
|
||||
*/
|
||||
|
@ -1045,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."
|
||||
|
@ -1078,6 +1078,9 @@ resetPasswordConfirm: "パスワードリセットしますか?"
|
||||
sensitiveWords: "センシティブワード"
|
||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||
prohibitedWords: "禁止ワード"
|
||||
prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。"
|
||||
prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||
hiddenTags: "非表示ハッシュタグ"
|
||||
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||
|
@ -1043,6 +1043,7 @@ resetPasswordConfirm: "パスワード作り直すんでええな?"
|
||||
sensitiveWords: "けったいな単語"
|
||||
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
|
||||
sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。"
|
||||
prohibitedWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。"
|
||||
hiddenTags: "見えてへんハッシュタグ"
|
||||
hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。"
|
||||
notesSearchNotAvailable: "なんかノート探せへん。"
|
||||
|
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
|
||||
sensitiveWords: "민감한 단어"
|
||||
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
|
||||
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
|
||||
prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
|
||||
hiddenTags: "숨긴 해시태그"
|
||||
hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다."
|
||||
notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다."
|
||||
|
@ -1015,6 +1015,7 @@ resetPasswordConfirm: "Сбросить пароль?"
|
||||
sensitiveWords: "Чувствительные слова"
|
||||
sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк."
|
||||
sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
|
||||
prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
|
||||
notesSearchNotAvailable: "Поиск заметок недоступен"
|
||||
license: "Лицензия"
|
||||
unfavoriteConfirm: "Удалить избранное?"
|
||||
|
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุ
|
||||
sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน"
|
||||
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||
sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
|
||||
prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
|
||||
hiddenTags: "แฮชแท็กที่ซ่อนอยู่"
|
||||
hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่"
|
||||
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน"
|
||||
|
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "确定重置密码?"
|
||||
sensitiveWords: "敏感词"
|
||||
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
|
||||
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
|
||||
prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
|
||||
hiddenTags: "隐藏标签"
|
||||
hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。"
|
||||
notesSearchNotAvailable: "帖子检索不可用"
|
||||
|
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "重設密碼?"
|
||||
sensitiveWords: "敏感詞"
|
||||
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
|
||||
sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。"
|
||||
prohibitedWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。"
|
||||
hiddenTags: "隱藏標籤"
|
||||
hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。"
|
||||
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sharkey",
|
||||
"version": "2024.2.0-beta.10",
|
||||
"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",
|
||||
|
16
packages/backend/migration/1707429690000-prohibited-words.js
Normal file
16
packages/backend/migration/1707429690000-prohibited-words.js
Normal 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"`);
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
@ -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,
|
||||
|
@ -612,7 +612,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 +647,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 +751,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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -77,12 +77,12 @@ 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');
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
@ -636,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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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: '{}',
|
||||
})
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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', () => {
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
@ -29,8 +29,8 @@
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@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",
|
||||
"@vue/compiler-sfc": "3.4.18",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"buraha": "0.0.1",
|
||||
@ -72,8 +72,8 @@
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.7.2",
|
||||
"vite": "5.0.12",
|
||||
"vue": "3.4.15",
|
||||
"vite": "5.1.0",
|
||||
"vue": "3.4.18",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -97,12 +97,12 @@
|
||||
"@storybook/types": "7.6.10",
|
||||
"@storybook/vue3": "7.6.10",
|
||||
"@storybook/vue3-vite": "7.6.10",
|
||||
"@testing-library/vue": "8.0.1",
|
||||
"@testing-library/vue": "8.0.2",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@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",
|
||||
@ -112,10 +112,10 @@
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"@vue/runtime-core": "3.4.18",
|
||||
"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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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({});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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({});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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 })));
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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([
|
||||
'気象警報注意報',
|
||||
'気象警報',
|
||||
'気象情報',
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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));
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -19,7 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ChannelConnection as Connection } from 'misskey-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
@ -90,8 +89,8 @@ function prepend(note) {
|
||||
}
|
||||
}
|
||||
|
||||
let connection: Connection;
|
||||
let connection2: Connection;
|
||||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let paginationQuery: Paging | null = null;
|
||||
|
||||
const stream = useStream();
|
||||
@ -163,7 +162,7 @@ function connectChannel() {
|
||||
roleId: props.role,
|
||||
});
|
||||
}
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||
}
|
||||
|
||||
function disconnectChannel() {
|
||||
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
||||
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
||||
scrolling="no"
|
||||
:allow="player.allow.join(';')"
|
||||
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||
:class="$style.playerIframe"
|
||||
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
||||
:style="{ border: 0 }"
|
||||
|
@ -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 { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
||||
@ -38,17 +38,17 @@ export const Default = {
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Misskey from 'misskey-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||
|
@ -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 { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
||||
@ -38,17 +38,17 @@ export const Default = {
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -7,7 +7,7 @@
|
||||
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 { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
import MkUrl from './MkUrl.vue';
|
||||
export const Default = {
|
||||
@ -59,8 +59,8 @@ export const Default = {
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.get('/url', (req, res, ctx) => {
|
||||
return res(ctx.json({
|
||||
http.get('/url', () => {
|
||||
return HttpResponse.json({
|
||||
title: 'Misskey Hub',
|
||||
icon: 'https://misskey-hub.net/favicon.ico',
|
||||
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
|
||||
@ -74,7 +74,7 @@ export const Default = {
|
||||
sitename: 'misskey-hub.net',
|
||||
sensitive: false,
|
||||
url: 'https://misskey-hub.net/',
|
||||
}));
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -49,6 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkTextarea v-model="prohibitedWords">
|
||||
<template #label>{{ i18n.ts.prohibitedWords }}</template>
|
||||
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkTextarea v-model="hiddenTags">
|
||||
<template #label>{{ i18n.ts.hiddenTags }}</template>
|
||||
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
|
||||
@ -87,6 +92,7 @@ const emailRequiredForSignup = ref<boolean>(false);
|
||||
const approvalRequiredForSignup = ref<boolean>(false);
|
||||
const bubbleTimelineEnabled = ref<boolean>(false);
|
||||
const sensitiveWords = ref<string>('');
|
||||
const prohibitedWords = ref<string>('');
|
||||
const hiddenTags = ref<string>('');
|
||||
const preservedUsernames = ref<string>('');
|
||||
const bubbleTimeline = ref<string>('');
|
||||
@ -99,6 +105,7 @@ async function init() {
|
||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
|
||||
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
||||
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
||||
hiddenTags.value = meta.hiddenTags.join('\n');
|
||||
preservedUsernames.value = meta.preservedUsernames.join('\n');
|
||||
tosUrl.value = meta.tosUrl;
|
||||
@ -115,6 +122,7 @@ function save() {
|
||||
tosUrl: tosUrl.value,
|
||||
privacyPolicyUrl: privacyPolicyUrl.value,
|
||||
sensitiveWords: sensitiveWords.value.split('\n'),
|
||||
prohibitedWords: prohibitedWords.value.split('\n'),
|
||||
hiddenTags: hiddenTags.value.split('\n'),
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
bubbleInstances: bubbleTimeline.value.split('\n'),
|
||||
|
@ -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 home_ from './home.vue';
|
||||
@ -39,12 +39,13 @@ export const Default = {
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/notes', (req, res, ctx) => {
|
||||
return res(ctx.json([]));
|
||||
http.post('/api/users/notes', () => {
|
||||
return HttpResponse.json([]);
|
||||
}),
|
||||
rest.get('/api/charts/user/notes', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
http.get('/api/charts/user/notes', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return HttpResponse.json({
|
||||
total: Array.from({ length }, () => 0),
|
||||
inc: Array.from({ length }, () => 0),
|
||||
dec: Array.from({ length }, () => 0),
|
||||
@ -54,11 +55,12 @@ export const Default = {
|
||||
renote: Array.from({ length }, () => 0),
|
||||
withFile: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
});
|
||||
}),
|
||||
rest.get('/api/charts/user/pv', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
http.get('/api/charts/user/pv', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return HttpResponse.json({
|
||||
upv: {
|
||||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
@ -67,7 +69,7 @@ export const Default = {
|
||||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -20,7 +20,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
|
||||
if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
|
||||
}
|
||||
|
||||
|
||||
if (theme.codeHighlighter) {
|
||||
let _res: ThemeRegistration = {};
|
||||
if (theme.codeHighlighter.base === '_none_') {
|
||||
@ -55,7 +55,7 @@ export async function getHighlighter(): Promise<Highlighter> {
|
||||
|
||||
export async function initHighlighter() {
|
||||
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
|
||||
|
||||
|
||||
await loadWasm(import('shiki/onig.wasm?init'));
|
||||
|
||||
// テーマの重複を消す
|
||||
@ -68,10 +68,7 @@ export async function initHighlighter() {
|
||||
themes,
|
||||
langs: [
|
||||
import('shiki/langs/javascript.mjs'),
|
||||
{
|
||||
aliases: ['is', 'ais'],
|
||||
...aiScriptGrammar.default,
|
||||
} as unknown as LanguageRegistration,
|
||||
aiScriptGrammar.default as unknown as LanguageRegistration,
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -116,6 +116,34 @@ describe('MkUrlPreview', () => {
|
||||
assert.strictEqual(iframe?.allow, 'fullscreen;web-share');
|
||||
});
|
||||
|
||||
test('A Summaly proxy response without allow falls back to the default', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
width: null,
|
||||
height: null,
|
||||
allow: undefined as any,
|
||||
},
|
||||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
assert.strictEqual(iframe?.allow, 'autoplay;encrypted-media;fullscreen');
|
||||
});
|
||||
|
||||
test('Filtering the allow list from the Summaly proxy', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
url: 'https://example.local',
|
||||
player: {
|
||||
url: 'https://example.local/player',
|
||||
width: null,
|
||||
height: null,
|
||||
allow: ['autoplay', 'camera', 'fullscreen'],
|
||||
},
|
||||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
assert.strictEqual(iframe?.allow, 'autoplay;fullscreen');
|
||||
});
|
||||
|
||||
test('Having a player width should keep the fixed aspect ratio', async () => {
|
||||
const iframe = await renderAndOpenPreview({
|
||||
url: 'https://example.local',
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2024.2.0-beta.10",
|
||||
"version": "2024.2.0-beta.11",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
@ -39,7 +39,7 @@
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@swc/jest": "0.2.31",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/node": "20.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
|
@ -4793,6 +4793,7 @@ export type operations = {
|
||||
hiddenTags: string[];
|
||||
blockedHosts: string[];
|
||||
sensitiveWords: string[];
|
||||
prohibitedWords: string[];
|
||||
bannedEmailDomains?: string[];
|
||||
preservedUsernames: string[];
|
||||
bubbleInstances: string[];
|
||||
@ -8814,6 +8815,7 @@ export type operations = {
|
||||
hiddenTags?: string[] | null;
|
||||
blockedHosts?: string[] | null;
|
||||
sensitiveWords?: string[] | null;
|
||||
prohibitedWords?: string[] | null;
|
||||
themeColor?: string | null;
|
||||
mascotImageUrl?: string | null;
|
||||
bannerUrl?: string | null;
|
||||
|
613
pnpm-lock.yaml
613
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user