diff --git a/.woodpecker/commit.yml b/.woodpecker/commit.yml index 6bb1b2d81..f57fd9d1e 100644 --- a/.woodpecker/commit.yml +++ b/.woodpecker/commit.yml @@ -1,7 +1,8 @@ pipeline: testCommit: - image: node:latest + image: node:alpine commands: + - apk add --no-cache cargo python3 make g++ - cp .config/ci.yml .config/default.yml - corepack enable - corepack prepare pnpm@latest --activate diff --git a/README.md b/README.md index af117ac4a..d05cb55db 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ - Notable differences: - Improved UI/UX (especially on mobile) - Improved notifications - - Fediverse account migration - Improved instance security - Improved accessibility + - Improved threads - Recommended Instances timeline - OCR image captioning - New and improved Groups diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 3de877dbe..2a72376de 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1,7 +1,7 @@ _lang_: "Català" headlineMisskey: "Una xarxa social de codi obert, descentralitzada i gratuita per\ \ sempre \U0001F680" -introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert, descentralitzada\ +introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert, descentralitzada\ \ i gratuita per sempre! \U0001F680" monthAndDay: "{day}/{month}" search: "Cercar" @@ -15,43 +15,43 @@ gotIt: "Ho he entès!" cancel: "Cancel·lar" enterUsername: "Introdueix el teu nom d'usuari" renotedBy: "Resignat per {user}" -noNotes: "Cap nota" +noNotes: "Cap publicació" noNotifications: "Cap notificació" -instance: "Instàncies" +instance: "Instància" settings: "Preferències" basicSettings: "Configuració bàsica" -otherSettings: "Configuració avançada" -openInWindow: "Obrir en una nova finestra" +otherSettings: "Altres opcions" +openInWindow: "Obrir en una finestra nova" profile: "Perfil" timeline: "Línia de temps" noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia." login: "Iniciar sessió" -loggingIn: "Identificant-se" -logout: "Tancar la sessió" +loggingIn: "Iniciant sessió" +logout: "Tancar sessió" signup: "Registrar-se" uploading: "Pujant..." save: "Desar" users: "Usuaris" addUser: "Afegir un usuari" -favorite: "Afegir a preferits" +favorite: "Afegir a favorits" favorites: "Favorits" -unfavorite: "Eliminar dels preferits" -favorited: "Afegit als preferits." -alreadyFavorited: "Ja s'ha afegit als preferits." -cantFavorite: "No s'ha pogut afegir als preferits." +unfavorite: "Eliminar de favorits" +favorited: "Afegit a favorits." +alreadyFavorited: "Ja s'ha afegit a favorits." +cantFavorite: "No s'ha pogut afegir a favorits." pin: "Fixar al perfil" -unpin: "Para de fixar del perfil" -copyContent: "Copiar el contingut" -copyLink: "Copiar l'enllaç" -delete: "Eliminar" -deleteAndEdit: "Esborrar i editar" -deleteAndEditConfirm: "Estàs segur que vols suprimir aquesta nota i editar-la? Perdràs\ - \ totes les reaccions, notes i respostes." -addToList: "Afegir a una llista" +unpin: "Deixar de fixar al perfil" +copyContent: "Còpia el contingut" +copyLink: "Còpia l'enllaç" +delete: "Esborra" +deleteAndEdit: "Esborrar i edita" +deleteAndEditConfirm: "Estàs segur que vols esborrar aquesta nota i editar-la? Perdràs\ + \ totes les reaccions, resignats i respostes." +addToList: "Afegir a la llista" sendMessage: "Enviar un missatge" -copyUsername: "Copiar nom d'usuari" -searchUser: "Cercar usuaris" -reply: "Respondre" +copyUsername: "Còpia nom d'usuari" +searchUser: "Cercar un usuari" +reply: "Respon" loadMore: "Carregar més" showMore: "Veure més" youGotNewFollower: "t'ha seguit" @@ -60,21 +60,21 @@ followRequestAccepted: "Sol·licitud de seguiment acceptada" mention: "Menció" mentions: "Mencions" directNotes: "Missatges directes" -importAndExport: "Importar / Exportar" +importAndExport: "Importar / Exportar Dades" import: "Importar" export: "Exportar" files: "Fitxers" -download: "Baixar" -driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes\ - \ associades a aquest fitxer adjunt també se suprimiran." +download: "Descarregar" +driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les publicacions\ + \ associades a aquest fitxer adjunt també es suprimiran." unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?" exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà\ - \ a la teva unitat un cop completat." + \ al teu Disc un cop completada." importRequested: "Has sol·licitat una importació. Això pot trigar una estona." lists: "Llistes" noLists: "No tens cap llista" -note: "Post" -notes: "Posts" +note: "Publicació" +notes: "Publicacions" following: "Seguint" followers: "Seguidors" followsYou: "Et segueix" @@ -83,7 +83,7 @@ manageLists: "Gestionar les llistes" error: "Error" somethingHappened: "S'ha produït un error" retry: "Torna-ho a intentar" -pageLoadError: "S'ha produït un error en carregar la pàgina" +pageLoadError: "Alguna cosa a sortit malament al carregar la pàgina." pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria\ \ cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després\ \ d'esperar una estona." @@ -100,13 +100,13 @@ followRequests: "Sol·licituds de seguiment" unfollow: "Deixar de seguir" followRequestPending: "Sol·licituds de seguiment pendents" enterEmoji: "Introduir un emoji" -renote: "Renotar" -unrenote: "Anul·lar renota" -renoted: "Renotat." -cantRenote: "Aquesta publicació no pot ser renotada." -cantReRenote: "Impossible renotar una renota." +renote: "Impulsà" +unrenote: "Anul·lar impuls" +renoted: "Impulsat." +cantRenote: "Aquesta publicació no pot ser impulsada." +cantReRenote: "No es pot impulsar un impuls." quote: "Citar" -pinnedNote: "Nota fixada" +pinnedNote: "Publicació fixada" pinned: "Fixar al perfil" you: "Tu" clickToShow: "Fes clic per mostrar" @@ -116,7 +116,7 @@ reaction: "Reaccions" reactionSetting: "Reaccions a mostrar al selector de reaccions" reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem\ \ \"+\" per afegir." -rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" +rememberNoteVisibility: "Recorda la configuració de visibilitat de les publicacions" attachCancel: "Eliminar el fitxer adjunt" markAsSensitive: "Marcar com a NSFW" unmarkAsSensitive: "Deixar de marcar com a sensible" @@ -130,7 +130,7 @@ unsuspend: "Deixa de suspendre" instances: "Instàncies" remove: "Eliminar" nsfw: "NSFW" -pinnedNotes: "Nota fixada" +pinnedNotes: "Publicació fixada" userList: "Llistes" smtpUser: "Nom d'usuari" smtpPass: "Contrasenya" @@ -140,14 +140,84 @@ file: "Fitxers" _email: _follow: title: "t'ha seguit" + _receiveFollowRequest: + title: Heu rebut una sol·licitud de seguiment _mfm: mention: "Menció" quote: "Citar" search: "Cercar" + dummy: Calckey amplia el món del Fediverse + hashtag: Etiqueta + intro: MFM és un llenguatge de marques utilitzat a Misskey, Calckey, Akkoma i més + que es pot utilitzar en molts llocs. Aquí podeu veure una llista de tota la sintaxi + MFM disponible. + hashtagDescription: Podeu especificar un hashtag mitjançant un signe de coixinet + i un text. + url: URL + urlDescription: Es poden mostrar URL. + link: Enllaç + linkDescription: Parts específiques del text es poden mostrar com a URL. + bold: Negreta + boldDescription: Ressalta les lletres fent-les més gruixudes. + smallDescription: Mostra contingut petit i prim. + small: Petit + centerDescription: Mostra el contingut centrat. + inlineCode: Codi (en línia) + inlineMathDescription: Mostra fórmules matemàtiques (KaTeX) en línia + blockCode: Codi (Bloc) + blockCodeDescription: Mostra el ressaltat de sintaxi per al codi de diverses línies + (programa) en un bloc. + inlineMath: Matemàtiques (en línia) + jellyDescription: Dóna al contingut una animació semblant a una gelatina. + bounceDescription: Ofereix al contingut una animació de rebot. + jumpDescription: Dóna al contingut una animació de salt. + shake: Animació (Shake) + shakeDescription: Dóna al contingut una animació tremolosa. + bounce: Animació (Bounce) + x3Description: Mostra contingut encara més gran. + x2Description: Mostra contingut més gran. + twitchDescription: Ofereix al contingut una animació fortament convulsa. + spin: Animació (Spin) + spinDescription: Dóna al contingut una animació giratòria. + x2: Gran + x3: Molt gran + x4: Increïblement gran + blur: Desenfocament + x4Description: Mostra contingut fins i tot més gran que gran que gran. + rainbowDescription: Fa que el contingut aparegui en colors de l'arc de Sant Martí. + sparkle: Brillantor + sparkleDescription: Dóna al contingut un efecte de partícula brillant. + rotate: Girar + rotateDescription: Gira el contingut en un angle especificat. + positionDescription: Mou el contingut en una quantitat especificada. + fontDescription: Estableix el tipus de lletra en què voleu mostrar el contingut. + position: Posició + rainbow: Arc de Sant Martí + jelly: Animació (Jelly) + tada: Animació (Tada) + tadaDescription: Dóna al contingut una animació tipus "Tada!". + jump: Animació (Jump) + twitch: Animació (Twitch) + blurDescription: Desenfoca el contingut. Es mostrarà clarament quan passeu el cursor + per sobre. + font: Tipus de lletra + cheatSheet: Full de trucs de MFM + mentionDescription: Podeu especificar un usuari mitjançant un arrova i un nom d'usuari. + center: Centre + inlineCodeDescription: Mostra el ressaltat de sintaxi en línia per al codi (de programa). + blockMath: Matemàtiques (Bloc) + blockMathDescription: Mostra fórmules matemàtiques (KaTeX) en un bloc + quoteDescription: Mostra el contingut com una cita. + emoji: Emoji personalitzat + emojiDescription: Un emoji personalitzat és pot mostrar envoltant el nom amb dos + punts. + searchDescription: Mostra un quadre de cerca amb el text introduït prèviament. + flip: Capgirar + flipDescription: Capgira el contingut horitzontalment o verticalment. _theme: keys: mention: "Menció" - renote: "Renotar" + renote: "Impulsar" _sfx: note: "Posts" notification: "Notificacions" @@ -180,10 +250,101 @@ _pages: arg1: "Llistes" _seedRandomPick: arg2: "Llistes" + arg1: Llavor _pick: arg1: "Llistes" _listLen: arg1: "Llistes" + add: Afegir + _subtract: + arg1: A + arg2: B + subtract: Restar + _round: + arg1: Número + eq: A i B són iguals + _mod: + arg2: B + arg1: A + round: Arrodoniment decimal + _and: + arg1: A + arg2: B + or: A O B + _or: + arg1: A + arg2: B + lt: < A és menor que B + _lt: + arg1: A + arg2: B + gt: '> A és més gran que B' + _gt: + arg1: A + arg2: B + seedRannum: Nombre aleatori (amb llavor) + _seedRannum: + arg1: Llavor + arg2: Valor mínim + arg3: Valor màxim + _eq: + arg1: A + arg2: B + ltEq: <= A és menor o igual que B + _multiply: + arg2: B + arg1: A + divide: Dividir + notEq: A i B són diferents + _notEq: + arg1: A + arg2: B + and: A I B + _ltEq: + arg2: B + arg1: A + gtEq: '>= A és més gran o igual que B' + _gtEq: + arg1: A + arg2: B + if: Branca + _if: + arg1: Si + arg2: Aleshores + arg3: Altrament + not: NO + random: Aleatori + _dailyRandom: + arg1: Probabilitat + dailyRannum: Nombre aleatori (canvia un cop al dia per a cada usuari) + _add: + arg1: A + arg2: B + _divide: + arg1: A + arg2: B + mod: Resta + _not: + arg1: NO + _random: + arg1: Probabilitat + rannum: Nombre aleatori + _rannum: + arg1: Valor mínim + arg2: Valor màxim + randomPick: Tria aleatòriament de la llista + dailyRandom: Aleatori (canvia un cop al dia per a cada usuari) + _dailyRannum: + arg2: Valor màxim + arg1: Valor mínim + dailyRandomPick: Tria aleatòriament d'una llista (Canvis un cop al dia per a + cada usuari) + seedRandom: Aleatori (amb llavor) + _seedRandom: + arg1: Llavor + arg2: Probabilitat + seedRandomPick: Tria aleatòriament de la llista (amb llavor) + multiply: Multiplicar types: array: "Llistes" _notification: @@ -191,12 +352,12 @@ _notification: _types: follow: "Seguint" mention: "Menció" - renote: "Renotar" + renote: "Impulsos" quote: "Citar" reaction: "Reaccions" _actions: reply: "Respondre" - renote: "Renotar" + renote: "Impulsos" _deck: _columns: notifications: "Notificacions" @@ -469,3 +630,795 @@ enableLocalTimeline: Activa la línea de temps local enableRecommendedTimeline: Activa la línea de temps de recomanats pinnedClipId: ID del clip que vols fixar hcaptcha: hCaptcha +manageAntennas: Gestiona les Antenes +name: Nom +notesAndReplies: Articles i respostes +silence: Posa en silenci +withFiles: Amb fitxers +popularUsers: Usuaris populars +exploreUsersCount: Hi han {count} usuaris +exploreFediverse: Explora el Fesiverse +popularTags: Etiquetes populars +about: Sobre +recentlyUpdatedUsers: Usuaris actius fa poc +recentlyRegisteredUsers: Usuaris registrats fa poc +recentlyDiscoveredUsers: Nous suaris descoberts +administrator: Administrador +token: Token +registerSecurityKey: Registra una clau de seguretat +securityKeyName: Nom clau +lastUsed: Feta servir per última vegada +unregister: Anul·lar el registre +passwordLessLogin: Identificació sense contrasenya +share: Comparteix +notFound: No s'ha trobat +newPasswordIs: La nova contrasenya és "{password}" +notFoundDescription: No es pot trobar cap pàgina que correspongui a aquesta adreça + URL. +uploadFolder: Carpeta per defecte per pujar arxius +cacheClear: Netejar la memòria cau +markAsReadAllNotifications: Marca totes les notificacions com llegides +markAsReadAllUnreadNotes: Marca totes les publicacions com a llegides +markAsReadAllTalkMessages: Marca tots els missatges com llegits +help: Ajuda +inputMessageHere: Escriu aquí el missatge +close: Tancar +group: Grup +groups: Grups +createGroup: Crea un grup +ownedGroups: Grups que et pertanyen +joinedGroups: Grups als que t'has unit +groupName: Nom del grup +members: Membres +transfer: Transferir +messagingWithUser: Conversa privada +title: Títol +text: Text +enable: Activar +next: Següent +retype: Torna a entrar +noteOf: Publicat per {user} +inviteToGroup: Invitar a un grup +quoteAttached: Cita +quoteQuestion: Adjuntar com a cita? +noMessagesYet: Encara no hi han missatges +signinRequired: Si us plau registrat o inicia sessió per continuar +invitations: Invitacions +invitationCode: Codi d'invitació +checking: Comprovant... +usernameInvalidFormat: Pots fer servir lletres en majúscules o minúscules, nombres + i guions baixos. +tooShort: Massa curt +tooLong: Massa llarg +weakPassword: Contrasenya amb seguretat feble +strongPassword: Contrasenya amb seguretat forta +passwordMatched: Coincidències +signinWith: Inicieu sessió com {x} +signinFailed: No es pot iniciar sessió. El nom d'usuari o la contrasenya són incorrectes. +or: O +language: Idioma +uiLanguage: Idioma de la interfície d'usuari +groupInvited: T'han invitat a un grup +aboutX: Sobre {x} +youHaveNoGroups: No tens grups +disableDrawer: No facis servir els menús amb estil de calaix +noHistory: No ha historial disponible +signinHistory: Historial d'inicis de sessió +disableAnimatedMfm: Desactiva les animacions amb MFM +doing: Processant... +category: Categoría +existingAccount: El compte ja existeix +regenerate: Regenerar +docSource: Font d'aquest document +createAccount: Crear compte +fontSize: Mida del text +noFollowRequests: No tens cap sol·licitud de seguiment per aprovar +openImageInNewTab: Obre les imatges en una pestanya nova +dashboard: Panell +local: Local +remote: Remot +total: Total +weekOverWeekChanges: Canvis d'ençà la passada setmana +dayOverDayChanges: Canvis d'ençà ahir +appearance: Aparença +clientSettings: Configuració del client +accountSettings: Configuració del compte +promotion: Promogut +promote: Promoure +numberOfDays: Nombre de dies +objectStorageBaseUrl: Adreça URL base +hideThisNote: Amaga aquest article +showFeaturedNotesInTimeline: Mostra els articles destacats a la línea de temps +objectStorage: Emmagatzematge d'objectes +useObjectStorage: Fes servir l'emmagatzema d'objectes +expandTweet: Amplia el tuit +themeEditor: Editor de temes +description: Descripció +leaveConfirm: Hi han canvis que no s'han desat. Els vols descartar? +manage: Administració +plugins: Afegits +preferencesBackups: Preferències de còpies de seguretat +undeck: Treure el Deck +useBlurEffectForModal: Fes servir efectes de difuminació en les finestres modals +useFullReactionPicker: Fes servir el selector de reaccions a tamany complert +deck: Taulell +width: Amplada +generateAccessToken: Genera un token d'accés +medium: Mitja +small: Petit +permission: Permisos +enableAll: Activa tots +tokenRequested: Garantir accés al compte +pluginTokenRequestedDescription: Aquest afegit podrà fer servir els permisos configurats + aquí. +emailServer: Servidor de correu electrònic +notificationType: Tipus de notificació +edit: Editar +emailAddress: Adreça de Correu electrònic +smtpConfig: Configuració del servidor SMTP +smtpHost: Host +enableEmail: Activa la distribució de correu electrònic +smtpPort: Port +emailConfigInfo: Fet servir per confirmar les adreçats de correu electrònic al registrar-se + o si s'oblida la contrasenya +email: Correu electrònic +smtpSecure: Fes servir SSL/TLS implícit per connectar-se per SMTP +emptyToDisableSmtpAuth: Deixa el nom d'usuari i la contrasenya sense emplenar per + desactivar la verificació SMTP +smtpSecureInfo: Desactiva això quant facis servir STARTTLS +testEmail: Envia un correu electrònic de verificació +wordMute: Silenciar paraules +regexpError: Error a la Expressió Regular +regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de + la teva {tab} de paraules silenciades:' +userSaysSomething: '{name} va dir alguna cosa' +instanceMute: Silenciar instàncies +logs: Registres +copy: Copiar +delayed: Retardat +metrics: Mètriques +overview: Vista general +database: Base de dades +regenerateLoginToken: Regenera el token d'inici de sessió +reduceUiAnimation: Redueix les animacions de la UI +messagingWithGroup: Conversa en grup +invites: Invitacions +unavailable: No disponible +newMessageExists: Tens nous missatges +onlyOneFileCanBeAttached: Només pots adjuntar un fitxer per missatge +normalPassword: Contrasenya amb seguretat mitjana +passwordNotMatched: No hi han coincidències +useOsNativeEmojis: Fes servir els emojis per defecte del Sistema Operatiu +joinOrCreateGroup: Fes que et convidin a un grup o crea el teu propi. +objectStorageBaseUrlDesc: "Es l'adreça URL que serveix com a referència. Específica\ + \ la adreça URL del CDN o Proxy si fas servir.\nPer fer servir S3 'https://.s3.amazonaws.com'\ + \ i per GCS o serveis semblants 'https://storage.googleapis.com/', etc." +height: Alçada +large: Gran +notificationSetting: Preferències de notificacions +makeActive: Activar +notificationSettingDesc: Tria el tipus de notificació que es veure. +notifyAntenna: Notificar noves articles +withFileAntenna: Només articles amb fitxers +enableServiceworker: Activa les notificacions push per al teu navegador +antennaUsersDescription: Escriu un nom d'usuari per línea +antennaInstancesDescription: Escriu la adreça d'una instància per línea +tags: Etiquetes +antennaSource: Font de la antena +antennaKeywords: Paraules claus a escolta +antennaExcludeKeywords: Paraules clau a excluir +antennaKeywordsDescription: Separades amb espais per fer una condició AND i amb una + línea nova per fer una condició OR. +caseSensitive: Sensible a majúscules i minúscules +withReplies: Inclou respostes +connectedTo: Aquest(s) compte(s) estan connectats +silenceConfirm: Segur que vols posa en silenci aquest usuari? +unsilence: Desfés posar en silenci +unsilenceConfirm: Segur que vols treure el silenci a aquest usuari? +aboutMisskey: Sobre Calckey +twoStepAuthentication: Autentificació de dos factors +moderator: Moderador +moderation: Moderació +available: Disponible +tapSecurityKey: Escriu la teva clau de seguretat +nUsersMentioned: Esmentat per {n} usuari(s) +securityKey: Clau de seguretat +resetPassword: Restablir contrasenya +describeFile: Afegeix un subtítol +enterFileDescription: Entra un subtítol +author: Autor +disableAll: Desactiva tots +userSaysSomethingReason: '{name} va dir {reason}' +display: Visualització +channel: Canals +create: Crear +useGlobalSetting: Fes servir els ajustos globals +useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del + teu compte. Si es desactiva , es poden fer configuracions individuals. +other: Altres +menu: Menú +addItem: Afegeix un element +divider: Divisor +relays: Relés +addRelay: Afegeix un Relé +inboxUrl: Adreça de la safata d'entrada +addedRelays: Relés afegits +serviceworkerInfo: Ha de estar activat per les notificacions push. +poll: Enquesta +deletedNote: Article eliminat +disablePlayer: Tancar el reproductor de vídeo +fileIdOrUrl: ID o adreça URL del fitxer +behavior: Comportament +regenerateLoginTokenDescription: Regenera el token que es fa servir de manera interna + durant l'inici de sessió. Normalment això no és necessari. Si es torna a genera + el token, es tancarà la sessió a tots els dispositius. +setMultipleBySeparatingWithSpace: Separa diferents entrades amb espais. +reportAbuseOf: Informa sobre {name} +sample: Exemple +abuseReports: Informes +reportAbuse: Informe +reporter: Informador +reporterOrigin: Origen d'el informador +forwardReport: Envia l'informe a una instancia remota +abuseReported: El teu informe ha sigut enviat. Moltes gràcies. +reporteeOrigin: Origen de l'informe +send: Enviar +abuseMarkAsResolved: Marcar l'informe com a resolt +visibility: Visibilitat +useCw: Amaga el contingut +enablePlayer: Obre el reproductor de vídeo +yourAccountSuspendedDescription: Aquest compte ha sigut suspesa per no seguir els + termes de servei del servidor o quelcom similar. Contacte amb l'administrador si + vols conèixer la raó amb més detall. Si us plau no facis un compte nou. +invisibleNote: Article ocult +enableInfiniteScroll: Carregar més de forma automàtica +fillAbuseReportDescription: Si us plau omple els detalls sobre aquest informe. Si + es sobre un article en concret, si us plau inclou l'adreça URL. +forwardReportIsAnonymous: Com a informador a l'instància remota no es mostrarà el + teu compte, si no un compte anònim. +openInNewTab: Obrir en una pestanya nova +openInSideView: Obrir a la vista lateral +defaultNavigationBehaviour: Navegació per defecte +editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé + el teu compte. +userSilenced: Aquest usuari ha sigut silenciat. +instanceTicker: Informació de notes de l'instància +waitingFor: Esperant a {x} +random: Aleatori +system: Sistema +switchUi: Interfície d'usuari +createNewClip: Crear un clip nou +unclip: Treure clip +public: Públic +renotesCount: Nombre de re-notes fetes +sentReactionsCount: Nombre de reaccions fetes +receivedReactionsCount: Nombre de reaccions rebudes +pollVotesCount: Nombre de vots fets en enquestes +pollVotedCount: Nombre de vots rebuts en enquestes +yes: Sí +no: No +noCrawle: Rebutjar la indexació dels restrejadors +driveUsage: Espai fet servir al Disk +noCrawleDescription: No permetre que els buscadors guardin la informació de les pàgines + de perfil, notes, Pàgines, etc. +alwaysMarkSensitive: Marcar per defecte com a NSFW +lockedAccountInfo: Només si has configurat la visibilitat del compte per "Només seguidors" + les teves notes no serem visibles per a ningú, inclús si has d'aprovar els teus + seguiments manualment. +disableShowingAnimatedImages: No reproduir les imatges animades +verificationEmailSent: S'ha enviat correu electrònic de verificació. Si us plau segueix + les instruccions per completar la verificació. +notSet: Sense especificar +emailVerified: Correu electrònic enviat +loadRawImages: Carregar les imatges originals en comptes de mostrar les miniatures +noteFavoritesCount: Nombre de notes afegides a favorits +useSystemFont: Fes servir la font per defecte del sistema +contact: Contacte +clips: Clips +experimentalFeatures: Característiques experimentals +developer: Desenvolupador +makeExplorableDescription: Si desactives aquesta funció el teu compte no sortirà a + la secció "Explora". +showGapBetweenNotesInTimeline: Mostra un espai entre notes a la línea de temps +makeExplorable: Fes el compte visible a "Explora" +duplicate: Duplicar +left: Esquerra +wide: Ample +narrow: Estret +reloadToApplySetting: Aquesta configuració només sortirà efecte després de recarregar + la pàgina. Vols fer-ho ara? +needReloadToApply: Es requereix recarregar la pàgina perquè això surti efecte. +showTitlebar: Mostrar la barra de títol +onlineUsersCount: Hi han {n} usuaris connectats +nUsers: '{n} Usuaris' +nNotes: '{n} Notes' +sendErrorReports: Enviar informe d'error +clearCache: Netejar memòria cau +switchAccount: Canvia de compte +enabled: Activat +configure: Configurar +noBotProtectionWarning: La protecció contra bots no està configurada. +ads: Anuncis +ratio: Ratio +global: Global +sent: Enviat +received: Rebut +whatIsNew: Mostra els canvis +usernameInfo: Un nom que identifica el vostre compte d'altres en aquest servidor. + Podeu utilitzar l'alfabet (a~z, A~Z), els dígits (0~9) o el guió baix (_). Els noms + d'usuari no es poden canviar més tard. +breakFollow: Suprimeix el seguidor +makeReactionsPublicDescription: Això farà que la llista de totes les vostres reaccions + passades sigui visible públicament. +hide: Amagar +leaveGroupConfirm: Estàs segur que vols deixar "{nom}"? +voteConfirm: Vols confirmar el teu vot per a "{choice}"? +leaveGroup: Sortir del grup +rateLimitExceeded: S'ha excedit el límit proporcionat +cropImage: Retalla la imatge +cropImageAsk: Vols retallar aquesta imatge? +failedToFetchAccountInformation: No s'ha pogut obtenir la informació del compte +driveCapOverrideCaption: Restableix la capacitat per defecte introduint un valor de + 0 o inferior. +type: Tipus +label: Etiqueta +beta: Beta +navbar: Barra de navegació +adminCustomCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què fa. La + introducció de valors inadequats pot fer que els clients de TOTS deixin de funcionar + amb normalitat. Assegureu-vos que el vostre CSS funcioni correctament provant-lo + a la configuració de l'usuari. +showUpdates: Mostra una finestra emergent quan Calckey s'actualitzi +recommendedInstances: Instàncies recomanades +recommendedInstancesDescription: Instàncies recomanades separades per salts de línia + per aparèixer a la línia de temps recomanada. NO afegiu `https://`, NOMÉS el domini. +caption: Descripció Automàtica +splash: Pantalla de Benvinguda +swipeOnDesktop: Permet lliscar a l'estil del mòbil a l'escriptori +updateAvailable: Pot ser que hi hagi una actualització disponible! +logoImageUrl: URL de la imatge del logotip +showAdminUpdates: Indica que hi ha disponible una versió nova de Calckey (només per + a administradors) +replayTutorial: Repetició del tutorial +migration: Migració +moveAccountDescription: Aquest procés és irreversible. Assegureu-vos que hàgiu configurat + un àlies per a aquest compte al vostre compte nou abans de moure's. Introduïu l'etiqueta + del compte amb el format @person@instance.com +moveToLabel: 'Compte al qual us moveu:' +moveAccount: Mou el compte! +moveFromDescription: Això establirà un àlies del vostre compte antic perquè pugueu + passar d'aquest compte a aquest actual. Feu això ABANS de moure's del vostre compte + anterior. Introduïu l'etiqueta del compte amb el format @person@instance.com +_sensitiveMediaDetection: + description: Redueix l'esforç de moderació del servidor mitjançant el reconeixement + automàtic dels mitjans NSFW mitjançant l'aprenentatge automàtic. Això augmentarà + lleugerament la càrrega al servidor. + setSensitiveFlagAutomaticallyDescription: Els resultats de la detecció interna es + conservaran encara que aquesta opció estigui desactivada. + analyzeVideos: Activa l'anàlisi de vídeos + analyzeVideosDescription: Analitza vídeos a més d'imatges. Això augmentarà lleugerament + la càrrega al servidor. + setSensitiveFlagAutomatically: Marca com a NSFW + sensitivity: Sensibilitat de detecció + sensitivityDescription: La reducció de la sensibilitat comportarà menys deteccions + errònies (falsos positius), mentre que augmentar-la comportarà menys deteccions + falses (falsos negatius). +_emailUnavailable: + used: Aquesta adreça de correu electrònic ja s'està utilitzant + format: El format d'aquesta adreça de correu electrònic no és vàlid + disposable: Les adreces de correu electrònic d'un sol ús no es poden utilitzar + mx: Aquest servidor de correu electrònic no és vàlid + smtp: Aquest servidor de correu electrònic no respon +_ffVisibility: + public: Públic + followers: Visible només per als seguidors + private: Privat +_signup: + emailAddressInfo: Introduïu la vostra adreça de correu electrònic. No es farà públic. + almostThere: Gairebé està + emailSent: S'ha enviat un correu electrònic de confirmació a la vostra adreça electrònica + ({email}). Feu clic a l'enllaç inclòs per completar la creació del compte. +_accountDelete: + started: S'ha iniciat la supressió. + accountDelete: Suprimeix el compte + mayTakeTime: Com que la supressió del compte és un procés que requereix molts recursos, + pot ser que trigui algun temps a completar-se en funció de la quantitat de contingut + que hàgiu creat i de quants fitxers hàgiu penjat. + sendEmail: Un cop s'hagi completat la supressió del compte, s'enviarà un correu + electrònic a l'adreça de correu electrònic registrada en aquest compte. + inProgress: La supressió del compte està en curs + requestAccountDelete: Sol·licitar la supressió del compte +_ad: + back: Enrera + reduceFrequencyOfThisAd: Mostrar aquest anunci menys +_gallery: + my: La meva Galeria + liked: Notes que m'han agradat + unlike: Elimina m'agrada + like: M'agrada +_forgotPassword: + contactAdmin: Aquesta instància no admet l'ús d'adreces de correu electrònic; poseu-vos + en contacte amb l'administrador de la instància per restablir la contrasenya. + ifNoEmail: Si no heu utilitzat cap correu electrònic durant el registre, poseu-vos + en contacte amb l'administrador de la instància. + enterEmail: Introduïu l'adreça de correu electrònic que heu utilitzat per registrar-vos. + A continuació, se li enviarà un enllaç amb el qual podeu restablir la vostra contrasenya. +_plugin: + install: Instal·leu connectors + installWarn: Si us plau, no instal·leu connectors que no siguin fiables. + manage: Gestionar els connectors +_preferencesBackups: + saveNew: Desa una còpia de seguretat nova + apply: Aplicar a aquest dispositiu + loadFile: Carrega des del fitxer + save: Desa els canvis + nameAlreadyExists: Ja existeix una còpia de seguretat anomenada "{name}". Introduïu + un nom diferent. + renameConfirm: Canviar el nom d'aquesta còpia de seguretat de "{old}" a "{new}"? + noBackups: No existeixen còpies de seguretat. Podeu fer una còpia de seguretat de + la configuració del vostre client en aquest servidor utilitzant "Crea una còpia + de seguretat nova". + deleteConfirm: Vols suprimir la còpia de seguretat anomanada {name}? + updatedAt: 'Actualitzat el: {time} {date}' + createdAt: 'Creat el: {time} {date}' + cannotLoad: No s'ha pogut carregar + inputName: Introduïu un nom per a aquesta còpia de seguretat + saveConfirm: Deseu la còpia de seguretat com a {name}? + invalidFile: Format de fitxer no vàlid + applyConfirm: Realment voleu aplicar la còpia de seguretat "{name}" a aquest dispositiu? + La configuració existent d'aquest dispositiu es sobreescriurà. + list: Còpies de seguretat creades + cannotSave: S'ha produït un error en desar +_registry: + domain: Domini + createKey: Crea la clau + scope: Àmbit + key: Clau + keys: Claus +silenced: Silenciat +objectStorageUseSSL: Fes servir SSL +yourAccountSuspendedTitle: Aquest compte està suspès +i18nInfo: Calckey està sent traduïts a diversos idiomes per voluntaris. Pots ajudar + {link}. +manageAccessTokens: Administrar tokens d'accés +accountInfo: Informació del compte +pageLikedCount: Nombre de m'agrada rebuts a Pàgines +center: Centre +registry: Registre +closeAccount: Tancar el compte +currentVersion: Versió actual +latestVersion: Versió més nova +newVersionOfClientAvailable: Aquesta és la versió del client més nova disponible. +usageAmount: Ús +capacity: Capacitat +editCode: Editar codi +apply: Aplicar +repliesCount: Nombre de contestacions fetes +repliedCount: Nombre de respostes rebudes +renotedCount: Nombre d'impulsos rebuts +followingCount: Nombre de comptes seguits +followersCount: Nombre de seguidors +goBack: Enrera +quitFullView: Sortí de la vista complerta +addDescription: Afegeix una descripció +notSpecifiedMentionWarning: Aquesta nota conté mencions a usuaris no inclosos com + a destinataris +info: Sobre +hideOnlineStatus: Amagar l'estat de conexió +onlineStatus: Estat de conexió +online: En línea +offline: Desconectat +notRecommended: No recomanat +botProtection: Protecció contra Bots +instanceBlocking: Bloquejar/Silenciar Federació +selectAccount: Seleccionar un compte +disabled: Desactivat +quickAction: Accions ràpides +administration: Administració +switch: Canviar +gallery: Galeria +popularPosts: Pàgines populars +shareWithNote: Comparteix amb una nota +expiration: Data límit +memo: Memo +priority: Prioritat +high: Alt +middle: Mitjana +low: Baixa +emailNotConfiguredWarning: L'adreça de correu electrònic no està definida. +instanceSecurity: Seguretat de la instància +privateMode: Mode Privat +allowedInstances: Instàncies a la llista blanca +allowedInstancesDescription: Amfitrions d'instàncies a la llista blanca per a la federació, + cadascuna separat per una línia nova (només s'aplica en mode privat). +previewNoteText: Mostra la vista prèvia +customCss: CSS personalitzat +recommended: Recomanat +seperateRenoteQuote: Botons d'impuls i de citació separats +searchResult: Resultats de la cerca +hashtags: Etiquetes +troubleshooting: Resolució de problemes +learnMore: Aprèn més +misskeyUpdated: Calckey s'ha actualitzat! +translate: Tradueix +translatedFrom: Traduït per {x} +aiChanMode: Ai-chan a la interfície d'usuari clàssica +keepCw: Mantenir els avisos de contingut +pubSub: Comptes Pub/Sub +lastCommunication: Última comunicació +breakFollowConfirm: Confirmes que vols eliminar un seguidor? +itsOn: Activat +itsOff: Desactivat +emailRequiredForSignup: Requereix una adreça de correu electrònic per registrar-te +unread: Sense llegir +controlPanel: Tauler de control +manageAccounts: Gestionar comptes +makeReactionsPublic: Estableix l'historial de reaccions com a públic +classic: Clàssic +muteThread: Silenciar el fil +ffVisibility: Visibilitat dels Seguiments/Seguidors +incorrectPassword: Contrasenya incorrecta. +clickToFinishEmailVerification: Feu clic a [{ok}] per completar la verificació del + correu electrònic. +overridedDeviceKind: Tipus de dispositiu +smartphone: Smartphone +tablet: Tauleta +auto: Automàtic +recentNHours: Últimes {n} hores +recentNDays: Últims {n} dies +noEmailServerWarning: El servidor de correu electrònic no està configurat. +check: Comprovar +fast: Ràpida +sensitiveMediaDetection: Detecció de mitjans NSFW +remoteOnly: Només remotes +failedToUpload: S'ha produït un error en la càrrega +cannotUploadBecauseInappropriate: Aquest fitxer no s'ha pogut carregar perquè s'han + detectat parts d'aquest com a potencialment NSFW. +cannotUploadBecauseNoFreeSpace: La pujada ha fallat a causa de la manca de capacitat + del Disc. +enableAutoSensitive: Marcatge automàtic NSFW +moveTo: Mou el compte actual al compte nou +customKaTeXMacro: Macros KaTeX personalitzats +_aboutMisskey: + contributors: Col·laboradors principals + allContributors: Tots els col·laboradors + donate: Fes una donació a Calckey + source: Codi font + translation: Tradueix Calckey + about: Calckey és una bifurcació de Misskey feta per ThatOneCalculator, que està + en desenvolupament des del 2022. + morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí. + Gràcies! 🥰 + patrons: Mecenes de Calckey +unknown: Desconegut +pageLikesCount: Nombre de pàgines amb M'agrada +youAreRunningUpToDateClient: Estás fent servir la versió del client més nova. +unlikeConfirm: Vols treure el teu m'agrada? +fullView: Vista complerta +desktop: Escritori +notesCount: Nombre de notes +confirmToUnclipAlreadyClippedNote: Aquesta nota ja és al clip "{name}". Vols treure'l + d'aquest clip? +driveFilesCount: Nombre de fitxers el Disk +silencedInstances: Instàncies silenciades +silenceThisInstance: Silencia la instància +silencedInstancesDescription: Llista amb els noms de les instàncies que vols silenciar. + Les comptes en les instàncies silenciades seran tractades com "Silenciades", només + poden fer sol·licitud de seguiments, i no poden mencionar comptes locals si no les + segueixen. Això no afectarà les instàncies silenciades. +objectStorageEndpointDesc: Deixa això buit si fas servir AWS, S3, d'una altre manera + específica un "endpoint" com a '' o ':', depend del proveïdor + que facis servir. +objectStorageRegionDesc: Especifica una regió com a 'xx-east-1'. Si el teu proveïdor + no distingeix entre regions, deixa això en buit o pots escriure 'us-east-1'. +userPagePinTip: Pots mostrar notes aquí escollint "Pin al perfil" dintre del menú + de cada nota. +userInfo: Informació d'usuari +hideOnlineStatusDescription: Amagant el teu estat en línea redueix la comoditat d'ús + d'algunes característiques com ara la recerca. +active: Actiu +accounts: Comptes +postToGallery: Crea una nova nota a la galeria +secureMode: Mode segur (Recuperació Autoritzada) +customCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què fa. La introducció + de valors indeguts pot provocar que el client deixi de funcionar amb normalitat. +squareAvatars: Mostra avatars quadrats +secureModeInfo: Quan sol·liciteu des d'altres instàncies, no envieu de tornada sense + prova. +privateModeInfo: Quan està activat, només les instàncies de la llista blanca es poden + federar amb les vostres instàncies. Totes les publicacions s'amagaran al públic. +useBlurEffect: Utilitzeu efectes de desenfocament a la interfície d'usuari +accountDeletionInProgress: La supressió del compte està en curs +unmuteThread: Desfés el silenci al fil +deleteAccountConfirm: Això suprimirà el vostre compte de manera irreversible. Procedir? +requireAdminForView: Heu d'iniciar sessió amb un compte d'administrador per veure-ho. +enableAutoSensitiveDescription: Permet la detecció i el marcatge automàtics dels mitjans + NSFW mitjançant Machine Learning sempre que sigui possible. Fins i tot si aquesta + opció està desactivada, és possible que estigui habilitada a tota la instància. +localOnly: Només local +customKaTeXMacroDescription: "Configura macros per escriure expressions matemàtiques\ + \ fàcilment! La notació s'ajusta a les definicions de l'ordre LaTeX i s'escriu com\ + \ a \\newcommand{\\name}{content} o \\newcommand{\\name}[nombre d'arguments]{contingut}.\ + \ Per exemple, \\newcommand{\\add}[2]{#1 + #2} ampliarà \\add{3}{foo} a 3 + foo.\ + \ Els claudàtors que envolten el nom de la macro es poden canviar per claudàtors\ + \ rodons o quadrats. Això afecta els claudàtors utilitzats per als arguments. Es\ + \ pot definir una (i només una) macro per línia, i no podeu trencar la línia al\ + \ mig de la definició. Les línies no vàlides simplement s'ignoren. Només s'admeten\ + \ funcions de substitució de cadenes senzilles; La sintaxi avançada, com ara la\ + \ ramificació condicional, no es pot utilitzar aquí." +objectStorageRegion: Regió +objectStoragePrefix: Prefix +objectStoragePrefixDesc: Els fitxers es guardaran dins de carpetes amb aquest prefix. +objectStorageEndpoint: Endpoint +newNoteRecived: Hi han notes noves +sounds: Sons +listen: Escoltar +none: Res +showInPage: Mostrar a la página +popout: Apareixa +volume: Volum +objectStorageUseSSLDesc: Desactiva això si no fas servir HTTP per les connexions API +objectStorageUseProxy: Conectarse mitjançant un Proxy +objectStorageUseProxyDesc: Desactiva això si no faràs servir un servidor Proxy per + conexions API +objectStorageSetPublicRead: Fixar com a "public-read" al pujar +serverLogs: Registres del servidor +deleteAll: Esborrar tot +showFixedPostForm: Mostrar el formulari de publicació al principi de la línea de temps +unableToProcess: Aquesta operació no es pot acabar +recentUsed: Fet servir fa poc +install: Instal·lar +masterVolume: Volum principal +uninstall: Desinstal·lar +installedApps: Aplicacions autoritzades +nothing: No hi a res per veure +installedDate: Data d'autorització +details: Detalls +chooseEmoji: Selecciona un emoji +removeAllFollowingDescription: Fent això deixes de seguir tots els comptes de {host}. + Si us plau fes servir això sí, per exemple, l'instància deixa d'existir. +userSuspended: Aquest usuari ha sigut suspès. +lastUsedDate: Data d'últim ús +state: Estat +sort: Ordenar +ascendingOrder: Ascendent +descendingOrder: Descendent +scratchpad: Bloc de notes +scratchpadDescription: El bloc de notes proporciona un entorn per experiments amb + AiScript. Pots escriure, executar i comprovar els resultats interactuant amb Calckey. +output: Sortida +script: Script +disablePagesScript: Desactivar AiScript a les pàgines +updateRemoteUser: Actualitzar la informació de l'usuari remot +deleteAllFiles: Esborrar tots els fitxers +deleteAllFilesConfirm: Segur que vols esborrar tots els fitxers? +removeAllFollowing: Deixar de seguir a tots els usuaris +accentColor: Color principal +textColor: Color del text +value: Valor +sendErrorReportsDescription: "Quant està activat, es compartirà amb els desenvolupadors\ + \ de Calckey quant aparegui un problema quan ajudarà a millorar la qualitat.\nAixò\ + \ inclourà informació com la versió del teu sistema operatiu, el navegador que estiguis\ + \ fent servir, la teva activitat a Calckey, etc." +myTheme: El meu tema +backgroundColor: Color de fons +saveAs: Desa com... +advanced: Avançat +invalidValue: Valor invàlid. +createdAt: Dada de creació +updatedAt: Data d'actualització +saveConfirm: Desa canvis? +deleteConfirm: De veritat ho vols esborrar? +receiveAnnouncementFromInstance: Rep notificacions d'aquesta instància +emailNotification: Notificacions per correu electrònic +publish: Publicar +inChannelSearch: Buscar al canal +useReactionPickerForContextMenu: Obrir el selector de reaccions al fer click esquerra +typingUsers: L'{users} està escrivint +oneDay: Un dia +instanceDefaultLightTheme: Tema de llum predeterminat per a tota la instància +instanceDefaultDarkTheme: Tema fosc predeterminat per a tota la instància +instanceDefaultThemeDescription: Introduïu el codi del tema en format d'objecte. +mutePeriod: Durada del silenci +indefinitely: Permanentment +tenMinutes: 10 minuts +oneHour: Una hora +oneWeek: Una setmana +reflectMayTakeTime: Pot trigar una mica a reflectir-se. +thereIsUnresolvedAbuseReportWarning: Hi ha informes sense resoldre. +driveCapOverrideLabel: Canvieu la capacitat del disc per a aquest usuari +isSystemAccount: Un compte creat i operat automàticament pel sistema. +typeToConfirm: Introduïu {x} per confirmar +deleteAccount: Suprimeix el compte +document: Documentació +sendPushNotificationReadMessage: Suprimeix les notificacions push un cop s'hagin llegit + les notificacions o missatges rellevants +sendPushNotificationReadMessageCaption: Es mostrarà una notificació amb el text "{emptyPushNotificationMessage}" + durant un breu temps. Això pot augmentar l'ús de la bateria del vostre dispositiu, + si escau. +showAds: Mostrar anuncis +enterSendsMessage: Pren retorn al formulari del missatge per enviar (quant no s'activa + es Ctrl + Return) +customMOTD: MOTD personalitzat (missatges de la pantalla d'inici) +customMOTDDescription: Missatges personalitzats per al MOTD (pantalla de presentació) + separats per salts de línia es mostraran aleatòriament cada vegada que un usuari + carrega/recarrega la pàgina. +customSplashIcons: Icones personalitzades de la pantalla d'inici (urls) +customSplashIconsDescription: La URL de les icones de pantalla de presentació personalitzades + separades per salts de línia es mostraran aleatòriament cada vegada que un usuari + carrega/recarrega la pàgina. Si us plau, assegureu-vos que les imatges estiguin + en una URL estàtica, preferiblement totes a la mida de 192 x 192. +moveFrom: Mou a aquest compte des d'un compte anterior +moveFromLabel: 'Compte des del qual us moveu:' +migrationConfirm: "Esteu absolutament segur que voleu migrar el vostre compte a {account}?\ + \ Un cop ho feu, no podreu revertir-ho i no podreu tornar a utilitzar el vostre\ + \ compte amb normalitat.\nA més, assegureu-vos d'haver configurat aquest compte\ + \ actual com el compte del qual us moveu." +defaultReaction: Reacció d'emoji predeterminada per a notes sortints i entrants +enableCustomKaTeXMacro: Activa les macros KaTeX personalitzades +noteId: ID de la nota +_nsfw: + respect: Amaga els mitjans NSFW + ignore: No amagueu els mitjans NSFW + force: Amaga tots els mitjans +inUse: Utilitzat +ffVisibilityDescription: Et permet configurar qui pot veure a qui segueixes i qui + et segueix. +continueThread: Continuar el fil +reverse: Revés +objectStorageBucket: Cubell +objectStorageBucketDesc: Si us plau específica el nom del cubell que faràs servir + al teu proveïdor. +clip: Clip +createNew: Crear una nova +optional: Opcional +jumpToSpecifiedDate: Vés a una data concreta +showingPastTimeline: Ara es mostra un línea de temps antiga +clear: Tornar +markAllAsRead: Marcar tot com a llegit +recentPosts: Pàgines recents +noMaintainerInformationWarning: La informació del responsable no està configurada. +resolved: Resolt +unresolved: Sense resoldre +filter: Filtre +slow: Lenta +useDrawerReactionPickerForMobile: Mostra el selector de reaccions com a calaix al + mòbil +welcomeBackWithName: Benvingut de nou, {name} +showLocalPosts: 'Mostra les notes locals a:' +homeTimeline: Línea de temps Local +socialTimeline: Línea de temps Social +themeColor: Color del Ticker de la instància +size: Mida +numberOfColumn: Nombre de columnes +numberOfPageCache: Nombre de pàgines emmagatzemades a la memòria cau +numberOfPageCacheDescription: L'augment d'aquest nombre millorarà la comoditat dels + usuaris, però provocarà més càrrega del servidor i més memòria per utilitzar-la. +logoutConfirm: Vols tancar la sessió? +lastActiveDate: Data d'últim ús +statusbar: Barra d'estat +pleaseSelect: Selecciona una opció +colored: Color +refreshInterval: "Interval d'actualització " +speed: Velocitat +cannotUploadBecauseExceedsFileSizeLimit: Aquest fitxer no s'ha pogut carregar perquè + supera la mida màxima permesa. +activeEmailValidationDescription: Permet una validació més estricta de les adreces + de correu electrònic, que inclou la comprovació d'adreces d'un sol ús i si realment + es pot comunicar amb elles. Quan no està marcat, només es valida el format del correu + electrònic. +shuffle: Barrejar +account: Compte +move: Moure +pushNotification: Notificacions push +subscribePushNotification: Activar les notificacions push +unsubscribePushNotification: Desactivar les notificacions push +pushNotificationAlreadySubscribed: Les notificacions push ja estan activades +pushNotificationNotSupported: El vostre navegador o instància no admet notificacions + automàtiques +license: Llicència +indexPosts: Índex de notes +indexFrom: Índex a partir de l'identificador de notes (deixeu en blanc per indexar + cada publicació) +indexNotice: Ara indexant. Això probablement trigarà una estona, si us plau, no reinicieu + el servidor durant almenys una hora. diff --git a/locales/en-US.yml b/locales/en-US.yml index a11a404d6..4acb0fc5b 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -68,8 +68,8 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\ - \ with this file attached will also be deleted." +driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\ + \ will be removed from all posts that contain it as an attachment." unfollowConfirm: "Are you sure that you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added\ \ to your Drive once completed." @@ -197,6 +197,7 @@ perHour: "Per Hour" perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" +silenceThisInstance: "Silence this instance" operations: "Operations" software: "Software" version: "Version" @@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances that you want to\ \ block. Listed instances will no longer be able to communicate with this instance." +silencedInstances: "Silenced Instances" +silencedInstancesDescription: "List the hostnames of the instances that you want to\ + \ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances." hiddenTags: "Hidden Hashtags" hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\ \ to hide from trending and explore. Hidden hashtags are still discoverable via\ - \ other means." + \ other means. Blocked instances are not affected even if listed here." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -240,6 +244,7 @@ noCustomEmojis: "There are no emoji" noJobs: "There are no jobs" federating: "Federating" blocked: "Blocked" +silenced: "Silenced" suspended: "Suspended" all: "All" subscribing: "Subscribing" @@ -829,7 +834,7 @@ active: "Active" offline: "Offline" notRecommended: "Not recommended" botProtection: "Bot Protection" -instanceBlocking: "Blocked Instances" +instanceBlocking: "Federation Block/Silence" selectAccount: "Select account" switchAccount: "Switch account" enabled: "Enabled" @@ -1042,7 +1047,7 @@ moveFromLabel: "Account you're moving from:" moveFromDescription: "This will set an alias of your old account so that you can move\ \ from that account to this current one. Do this BEFORE moving from your older account.\ \ Please enter the tag of the account formatted like @person@instance.com" -migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\ +migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\ \ Once you do this, you won't be able to reverse it, and you won't be able to use\ \ your account normally again.\nAlso, please ensure that you've set this current\ \ account as the account you're moving from." @@ -1197,7 +1202,7 @@ _mfm: inlineMath: "Math (Inline)" inlineMathDescription: "Display math formulas (KaTeX) in-line" blockMath: "Math (Block)" - blockMathDescription: "Display multi-line math formulas (KaTeX) in a block" + blockMathDescription: "Display math formulas (KaTeX) in a block" quote: "Quote" quoteDescription: "Displays content as a quote." emoji: "Custom Emoji" @@ -1237,6 +1242,14 @@ _mfm: sparkleDescription: "Gives content a sparkling particle effect." rotate: "Rotate" rotateDescription: "Turns content by a specified angle." + position: "Position" + positionDescription: "Move content by a specified amount." + scale: "Scale" + scaleDescription: "Scale content by a specified amount." + foreground: "Foreground color" + foregroundDescription: "Change the foreground color of text." + background: "Background color" + backgroundDescription: "Change the background color of text." plain: "Plain" plainDescription: "Deactivates the effects of all MFM contained within this MFM\ \ effect." diff --git a/locales/fi.yml b/locales/fi.yml new file mode 100644 index 000000000..fd6c160e6 --- /dev/null +++ b/locales/fi.yml @@ -0,0 +1,223 @@ +username: Käyttäjänimi +fetchingAsApObject: Hae Fedeversestä +gotIt: Selvä! +cancel: Peruuta +enterUsername: Anna käyttäjänimi +renotedBy: Buustannut {user} +noNotes: Ei lähetyksiä +noNotifications: Ei ilmoituksia +instance: Instanssi +settings: Asetukset +basicSettings: Perusasetukset +otherSettings: Muut asetukset +openInWindow: Avaa ikkunaan +profile: Profiili +timeline: Aikajana +noAccountDescription: Käyttäjä ei ole vielä kirjoittanut kuvaustaan vielä. +login: Kirjaudu sisään +loggingIn: Kirjautuu sisään +logout: Kirjaudu ulos +uploading: Tallentaa ylös... +save: Tallenna +favorites: Kirjanmerkit +unfavorite: Poista kirjanmerkeistä +favorited: Lisätty kirjanmerkkeihin. +alreadyFavorited: Lisätty jo kirjanmerkkeihin. +cantFavorite: Ei voitu lisätä kirjanmerkkeihin. +pin: Kiinnitä profiiliin +unpin: Irroita profiilista +delete: Poista +forgotPassword: Unohtunut salasana +search: Etsi +notifications: Ilmoitukset +password: Salasana +ok: OK +noThankYou: Ei kiitos +signup: Rekisteröidy +users: Käyttäjät +addUser: Lisää käyttäjä +addInstance: Lisää instanssi +favorite: Lisää kirjanmerkkeihin +copyContent: Kopioi sisältö +deleteAndEdit: Poista ja muokkaa +copyLink: Kopioi linkki +makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä +follow: Seuraa +pinned: Kiinnitä profiiliin +followRequestPending: Seuraajapyyntö odottaa +you: Sinä +unrenote: Peruuta buustaus +reaction: Reaktiot +reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi, + paina "+" lisätäksesi. +attachCancel: Poista liite +enterFileName: Anna tiedostonimi +mute: Hiljennä +unmute: Poista hiljennys +headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on + ikuisesti ilmainen! 🚀 +monthAndDay: '{day}/{month}' +deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata + sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi. +addToList: Lisää listaan +sendMessage: Lähetä viesti +reply: Vastaa +loadMore: Lataa enemmän +showMore: Näytä enemmän +receiveFollowRequest: Seuraajapyyntö vastaanotettu +followRequestAccepted: Seuraajapyyntö hyväksytty +mentions: Maininnat +importAndExport: Tuo/Vie Tietosisältö +import: Tuo +export: Vie +files: Tiedostot +download: Lataa +unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}? +noLists: Sinulla ei ole listoja +note: Lähetys +notes: Lähetykset +following: Seuraa +createList: Luo lista +manageLists: Hallitse listoja +error: Virhe +somethingHappened: On tapahtunut virhe +retry: Yritä uudelleen +pageLoadError: Virhe ladattaessa sivua. +serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen. +youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi. +privacy: Tietosuoja +defaultNoteVisibility: Oletusnäkyvyys +followRequest: Seuraajapyyntö +followRequests: Seuraajapyynnöt +unfollow: Poista seuraaminen +enterEmoji: Syötä emoji +renote: Buustaa +renoted: Buustattu. +cantRenote: Tätä lähetystä ei voi buustata. +cantReRenote: Buustausta ei voi buustata. +quote: Lainaus +pinnedNote: Lukittu lähetys +clickToShow: Napsauta nähdäksesi +sensitive: Herkkää sisältöä (NSFW) +add: Lisää +enableEmojiReactions: Ota käyttöön emoji-reaktiot +showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa +reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa +rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset +markAsSensitive: Merkitse herkäksi sisällöksi (NSFW) +unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW) +renoteMute: Hiljennä buustit +renoteUnmute: Poista buustien hiljennys +block: Estä +unblock: Poista esto +unsuspend: Poista keskeytys +suspend: Keskeytys +blockConfirm: Oletko varma, että haluat estää tämän tilin? +unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston? +selectAntenna: Valitse antenni +selectWidget: Valitse vimpain +editWidgets: Muokkaa vimpaimia +editWidgetsExit: Valmis +emoji: Emoji +emojis: Emojit +emojiName: Emojin nimi +emojiUrl: Emojin URL-linkki +cacheRemoteFiles: Taltioi etätiedostot välimuistiin +flagAsBot: Merkitse tili botiksi +flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma. + Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat + vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät + käsittelemään tätä tiliä botina. +flagAsCat: Oletko kissa? 🐱 +flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa! +flagSpeakAsCat: Puhu kuin kissa +flagShowTimelineReplies: Näytä vastaukset aikajanalla +addAccount: Lisää tili +loginFailed: Kirjautuminen epäonnistui +showOnRemote: Katsele etäinstanssilla +general: Yleistä +accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:' +wallpaper: Taustakuva +setWallpaper: Aseta taustakuva +searchWith: 'Etsi: {q}' +youHaveNoLists: Sinulla ei ole listoja +followConfirm: Oletko varma, että haluat seurata käyttäjää {name}? +host: Isäntä +selectUser: Valitse käyttäjä +annotation: Kommentit +registeredAt: Rekisteröity +latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu +latestRequestSentAt: Viimeisin pyyntö lähetetty +storageUsage: Tallennustilan käyttö +charts: Kaaviot +stopActivityDelivery: Lopeta toimintojen lähettäminen +blockThisInstance: Estä tämä instanssi +operations: Toiminnot +metadata: Metatieto +monitor: Seuranta +jobQueue: Työjono +cpuAndMemory: Prosessori ja muisti +network: Verkko +disk: Levy +clearCachedFiles: Tyhjennä välimuisti +clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut + etätiedostot? +blockedInstances: Estetyt instanssit +hiddenTags: Piilotetut asiatunnisteet +mention: Maininta +copyUsername: Kopioi käyttäjänimi +searchUser: Etsi käyttäjää +showLess: Sulje +youGotNewFollower: seurasi sinua +directNotes: Yksityisviestit +driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset, + jotka sisältyvät tiedostoon, poistuvat myös. +importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken. +exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan + kun tuonti valmistuu. +lists: Listat +followers: Seuraajat +followsYou: Seuraa sinua +pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista. + Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen. +enterListName: Anna listalle nimi +withNFiles: '{n} tiedosto(t)' +instanceInfo: Instanssin tiedot +clearQueue: Tyhjennä jono +suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin? +unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen? +selectList: Valitse lista +customEmojis: Kustomoitu Emoji +addEmoji: Lisää +settingGuide: Suositellut asetukset +cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu + suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan + käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu. +flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa +flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien + lähetyksiin aikajanalla, jos se on päällä. +autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat +perHour: Tunnissa +removeWallpaper: Poista taustakuva +recipient: Vastaanottaja(t) +federation: Federaatio +software: Ohjelmisto +proxyAccount: Proxy-tili +proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien + etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän + luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen + käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan. +latestStatus: Viimeisin tila +selectInstance: Valitse instanssi +instances: Instanssit +perDay: Päivässä +version: Versio +statistics: Tilastot +clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon? +introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median + alusta, joka on ikuisesti ilmainen! 🚀 +clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät + federoidu. Yleensä tätä toimintoa ei tarvita. +blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää. + Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa. +_lang_: Suomi diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 466212ba2..8ae43cdb9 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -183,6 +183,7 @@ perHour: "1時間ごと" perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" blockThisInstance: "このインスタンスをブロック" +silenceThisInstance: "このインスタンスをサイレンス" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" blockedInstances: "ブロックしたインスタンス" blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。" +silencedInstances: "サイレンスしたインスタンス" +silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -220,6 +223,7 @@ noCustomEmojis: "絵文字はありません" noJobs: "ジョブはありません" federating: "連合中" blocked: "ブロック中" +silenced: "サイレンス中" suspended: "配信停止" all: "全て" subscribing: "購読中" @@ -768,7 +772,7 @@ active: "アクティブ" offline: "オフライン" notRecommended: "非推奨" botProtection: "Botプロテクション" -instanceBlocking: "インスタンスブロック" +instanceBlocking: "連合ブロック・サイレンス" selectAccount: "アカウントを選択" switchAccount: "アカウントを切り替え" enabled: "有効" @@ -1079,7 +1083,7 @@ _mfm: inlineMath: "数式(インライン)" inlineMathDescription: "数式(KaTeX)をインラインで表示します。" blockMath: "数式(ブロック)" - blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。" + blockMathDescription: "数式(KaTeX)をブロックで表示します。" quote: "引用" quoteDescription: "内容が引用であることを示せます。" emoji: "カスタム絵文字" @@ -1120,6 +1124,7 @@ _mfm: rotateDescription: "指定した角度で回転させます。" plain: "プレーン" plainDescription: "内側の構文を全て無効にします。" + position: 位置 _instanceTicker: none: "表示しない" remote: "リモートユーザーに表示" @@ -1128,7 +1133,7 @@ _serverDisconnectedBehavior: reload: "自動でリロード" dialog: "ダイアログで警告" quiet: "控えめに警告" - nothing: "何も起こらない" + nothing: "何もしない" _channel: create: "チャンネルを作成" edit: "チャンネルを編集" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 24878686b..91d96fcc5 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -986,7 +986,7 @@ _registry: createKey: "Новый ключ" _aboutMisskey: about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\ - \ начал с 2022." + \ началась с 2022." contributors: "Основные соавторы" allContributors: "Все соавторы" source: "Исходный код" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c652b52b7..645f11f56 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1009,9 +1009,9 @@ _mfm: blockCode: "代码(块)" blockCodeDescription: "语法高亮显示整块程序代码。" inlineMath: "数学公式(内嵌)" - inlineMathDescription: "显示内嵌的KaTex公式。" + inlineMathDescription: "显示内嵌的KaTeX公式。" blockMath: "数学公式(块)" - blockMathDescription: "显示整块的多行KaTex数学公式。" + blockMathDescription: "显示整块的KaTeX数学公式。" quote: "引用" quoteDescription: "可以用来表示引用的内容。" emoji: "自定义表情符号" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 78bf4f34c..c2dfd1ce0 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -64,7 +64,7 @@ import: "匯入" export: "匯出" files: "檔案" download: "下載" -driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n" +driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。" unfollowConfirm: "確定要取消追隨{name}嗎?" exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" importRequested: "已請求匯入。這可能會花一點時間" @@ -291,7 +291,7 @@ emptyDrive: "雲端硬碟為空" emptyFolder: "資料夾為空" unableToDelete: "無法刪除" inputNewFileName: "輸入檔案名稱" -inputNewDescription: "請輸入新標題 " +inputNewDescription: "請輸入新標題" inputNewFolderName: "輸入新資料夾的名稱" circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。" hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。" @@ -324,7 +324,7 @@ yearX: "{year}年" pages: "頁面" integration: "整合" connectService: "己連結" -disconnectService: "己斷開 " +disconnectService: "己斷開" enableLocalTimeline: "開啟本地時間軸" enableGlobalTimeline: "啟用公開時間軸" disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。" @@ -336,7 +336,7 @@ driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量" inMb: "以Mbps為單位" iconUrl: "圖像URL" bannerUrl: "橫幅圖像URL" -backgroundImageUrl: "背景圖片的來源網址 " +backgroundImageUrl: "背景圖片的來源網址" basicInfo: "基本資訊" pinnedUsers: "置頂用戶" pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。" @@ -490,7 +490,7 @@ useObjectStorage: "使用Object Storage" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" objectStorageBucket: "儲存空間(Bucket)" -objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " +objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。" objectStoragePrefix: "前綴" objectStoragePrefixDesc: "它存儲在此前綴目錄下。" objectStorageEndpoint: "端點(Endpoint)" @@ -560,8 +560,8 @@ disablePlayer: "關閉播放器" expandTweet: "展開推文" themeEditor: "主題編輯器" description: "描述" -describeFile: "添加標題 " -enterFileDescription: "輸入標題 " +describeFile: "添加標題" +enterFileDescription: "輸入標題" author: "作者" leaveConfirm: "有未保存的更改。要放棄嗎?" manage: "管理" @@ -865,7 +865,7 @@ driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideCaption: "如果指定0以下的值,就會被取消。" requireAdminForView: "必須以管理者帳號登入才可以檢視。" isSystemAccount: "由系統自動建立與管理的帳號。" -typeToConfirm: "要執行這項操作,請輸入 {x} " +typeToConfirm: "要執行這項操作,請輸入 {x}" deleteAccount: "刪除帳號" document: "文件" numberOfPageCache: "快取頁面數" @@ -876,7 +876,7 @@ statusbar: "狀態列" pleaseSelect: "請選擇" reverse: "翻轉" colored: "彩色" -refreshInterval: "更新間隔" +refreshInterval: "更新間隔 " label: "標籤" type: "類型" speed: "速度" @@ -895,7 +895,7 @@ activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址, navbar: "導覽列" shuffle: "隨機" account: "帳戶" -move: "移動 " +move: "移動" customKaTeXMacro: "自定義 KaTeX 宏" customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\ name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\ @@ -933,11 +933,11 @@ _accountDelete: inProgress: "正在刪除" _ad: back: "返回" - reduceFrequencyOfThisAd: "降低此廣告的頻率 " + reduceFrequencyOfThisAd: "降低此廣告的頻率" _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" - ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " - contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 " + ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。" + contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。" _gallery: my: "我的貼文" liked: "喜歡的貼文" @@ -1000,7 +1000,7 @@ _mfm: url: "URL" urlDescription: "可以展示URL位址。" link: "鏈接" - linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 " + linkDescription: "您可以將特定範圍的文章與 URL 相關聯。" bold: "粗體" boldDescription: "可以將文字顯示为粗體来強調。" small: "縮小" @@ -1012,9 +1012,9 @@ _mfm: blockCode: "程式碼(區塊)" blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。" inlineMath: "數學公式(內嵌)" - inlineMathDescription: "顯示內嵌的KaTex數學公式。" + inlineMathDescription: "顯示內嵌的KaTeX數學公式。" blockMath: "數學公式(方塊)" - blockMathDescription: "以區塊顯示複數行的KaTex數學式。" + blockMathDescription: "以區塊顯示KaTeX數學式。" quote: "引用" quoteDescription: "可以用來表示引用的内容。" emoji: "自訂表情符號" @@ -1805,3 +1805,6 @@ migration: 遷移 homeTimeline: 主頁時間軸 swipeOnDesktop: 允許在桌面上進行手機式滑動 logoImageUrl: 圖標網址 +addInstance: 增加一個實例 +noInstances: 沒有實例 +flagSpeakAsCat: 像貓一樣地說話 diff --git a/package.json b/package.json index 1f3b0e1a4..78e39e69f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "calckey", - "version": "13.2.0-dev38", + "version": "13.2.0-dev41", "codename": "aqua", "repository": { "type": "git", @@ -40,6 +40,8 @@ "@bull-board/ui": "^4.10.2", "@napi-rs/cli": "^2.15.0", "@tensorflow/tfjs": "^3.21.0", + "focus-trap": "^7.2.0", + "focus-trap-vue": "^4.0.1", "js-yaml": "4.1.0", "seedrandom": "^3.0.5" }, diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico index fd7aadf2e..9fb005f7b 100644 --- a/packages/backend/assets/favicon.ico +++ b/packages/backend/assets/favicon.ico @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce4ba88c40e8d79d697cead7ccad4325846c03995f429194e19a5aa1383b7a82 -size 3973 +oid sha256:03a16bbee6313e52ca17a04db25c4a8b312a3208421a7e351e734f4ad0184900 +size 483961 diff --git a/packages/backend/assets/favicon.png b/packages/backend/assets/favicon.png index b90943252..44d777ceb 100644 --- a/packages/backend/assets/favicon.png +++ b/packages/backend/assets/favicon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c259dc343a2824ee408c5e2daafa56abd0b31d29fd1f4c59be7d9d1fe0c5e379 -size 3951 +oid sha256:25f2b237d58b1094ff047ae1d8653fafeb1ae79b5997054f6997c4c252ab1a4e +size 6122 diff --git a/packages/backend/assets/favicon.svg b/packages/backend/assets/favicon.svg index 7f55f6312..4e00347fb 100644 --- a/packages/backend/assets/favicon.svg +++ b/packages/backend/assets/favicon.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f17cc091606efe4c5e6fc3dbf04b018bc169705f352d52c43dc771d5a716a1d -size 4285 +oid sha256:3d6f88a7b660f9960a5ed9ac4473ec447b46a0b79d02064639c05729519af39d +size 2890 diff --git a/packages/backend/assets/icons/192.png b/packages/backend/assets/icons/192.png index 51434dd78..48a863e34 100644 --- a/packages/backend/assets/icons/192.png +++ b/packages/backend/assets/icons/192.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:712381afff5aaf9f5ed479d8862a67ca5581307c5b807d7cfa3edf813cf214ad -size 7000 +oid sha256:ef1b3ac7867fa31073dd30b8bb90ef942d74446fadec5e663ae662bd3b74f8bd +size 7483 diff --git a/packages/backend/assets/icons/512.png b/packages/backend/assets/icons/512.png index 8c45a6311..e5ba760ad 100644 --- a/packages/backend/assets/icons/512.png +++ b/packages/backend/assets/icons/512.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27b9944477bdba4219e6f3af9af3393d947fbcc52f88b3d9ecf9df06547cd970 -size 7099 +oid sha256:23616706bb3475f88e355c7489f48306b37d0f6aa3de5cf5559ce357a5b728ba +size 21631 diff --git a/packages/backend/assets/inverse wordmark.png b/packages/backend/assets/inverse wordmark.png new file mode 100644 index 000000000..e5ba760ad --- /dev/null +++ b/packages/backend/assets/inverse wordmark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23616706bb3475f88e355c7489f48306b37d0f6aa3de5cf5559ce357a5b728ba +size 21631 diff --git a/packages/backend/assets/inverse wordmark.svg b/packages/backend/assets/inverse wordmark.svg index fe9a77be9..33d426136 100644 --- a/packages/backend/assets/inverse wordmark.svg +++ b/packages/backend/assets/inverse wordmark.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be36c9edc904f05d7f4a96f2092154b14cd7696fc2b9a317e77e56d85f1f06a0 -size 4395 +oid sha256:b034df14985fe2e6a3f2b37d80ed5144a2c75be2cf8393b236a71ef55e6432ba +size 2163 diff --git a/packages/backend/assets/splash.png b/packages/backend/assets/splash.png index 0547a8f19..1e396a741 100644 --- a/packages/backend/assets/splash.png +++ b/packages/backend/assets/splash.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a751c980d885ce137f65ae4baaacd89f46b8206774db3b90fb32ac7c06a3a8ed -size 9326 +oid sha256:67df7f31c099111ad56e98d75a71340c4274d27e0f38c979e941c1d5d073e3e8 +size 21007 diff --git a/packages/backend/migration/1682777547198-LibreTranslate.js b/packages/backend/migration/1682777547198-LibreTranslate.js new file mode 100644 index 000000000..dbaf483e6 --- /dev/null +++ b/packages/backend/migration/1682777547198-LibreTranslate.js @@ -0,0 +1,23 @@ +export class LibreTranslate1682777547198 { + name = "LibreTranslate1682777547198"; + + async up(queryRunner) { + await queryRunner.query(` + ALTER TABLE "meta" + ADD "libreTranslateApiUrl" character varying(512) + `); + await queryRunner.query(` + ALTER TABLE "meta" + ADD "libreTranslateApiKey" character varying(128) + `); + } + + async down(queryRunner) { + await queryRunner.query(` + ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey" + `); + await queryRunner.query(` + ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl" + `); + } +} diff --git a/packages/backend/migration/1682891890317-InstanceSilence.js b/packages/backend/migration/1682891890317-InstanceSilence.js new file mode 100644 index 000000000..f487111f7 --- /dev/null +++ b/packages/backend/migration/1682891890317-InstanceSilence.js @@ -0,0 +1,165 @@ +export class InstanceSilence1682891890317 { + name = "InstanceSilence1682891890317"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_renote_muting_createdAt"`, + ); + await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`); + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`, + ); + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`, + ); + await queryRunner.query( + `ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `, + ); + await queryRunner.query( + `ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + async down(queryRunner) { + await queryRunner.query( + `ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`, + ); + await queryRunner.query( + `ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`, + ); + await queryRunner.query( + `ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`, + ); + await queryRunner.query( + `ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`, + ); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`); + await queryRunner.query( + `ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `, + ); + await queryRunner.query( + `ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 7b92b8311..ce0b2c2e1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -26,7 +26,7 @@ "@bull-board/api": "^4.6.4", "@bull-board/koa": "^4.6.4", "@bull-board/ui": "^4.6.4", - "@calckey/megalodon": "5.1.24", + "@calckey/megalodon": "5.2.0", "@discordapp/twemoji": "14.0.2", "@elastic/elasticsearch": "7.17.0", "@koa/cors": "3.4.3", diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 4f367debe..0cd8c02ad 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -89,6 +89,11 @@ export type Source = { authKey?: string; isPro?: boolean; }; + libreTranslate: { + managed?: boolean; + apiUrl?: string; + apiKey?: string; + }; email: { managed?: boolean; address?: string; diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts index 6e4623242..35ed30793 100644 --- a/packages/backend/src/misc/should-block-instance.ts +++ b/packages/backend/src/misc/should-block-instance.ts @@ -18,3 +18,21 @@ export async function shouldBlockInstance( (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), ); } + +/** + * Returns whether a specific host (punycoded) should be limited. + * + * @param host punycoded instance host + * @param meta a resolved Meta table + * @returns whether the given host should be limited + */ +export async function shouldSilenceInstance( + host: Instance["host"], + meta?: Meta, +): Promise { + const { silencedHosts } = meta ?? (await fetchMeta()); + return silencedHosts.some( + (silencedHost) => + host === silencedHost || host.endsWith(`.${silencedHost}`), + ); +} diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 26a7c9c19..84f9af479 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -97,6 +97,11 @@ export class Meta { }) public blockedHosts: string[]; + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public silencedHosts: string[]; + @Column('boolean', { default: false, }) @@ -386,6 +391,18 @@ export class Meta { }) public deeplIsPro: boolean; + @Column('varchar', { + length: 512, + nullable: true, + }) + public libreTranslateApiUrl: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + }) + public libreTranslateApiKey: string | null; + @Column('varchar', { length: 512, nullable: true, diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts index fb4498911..667ec948d 100644 --- a/packages/backend/src/models/repositories/instance.ts +++ b/packages/backend/src/models/repositories/instance.ts @@ -1,12 +1,13 @@ import { db } from "@/db/postgre.js"; import { Instance } from "@/models/entities/instance.js"; import type { Packed } from "@/misc/schema.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; +import { + shouldBlockInstance, + shouldSilenceInstance, +} from "@/misc/should-block-instance.js"; export const InstanceRepository = db.getRepository(Instance).extend({ async pack(instance: Instance): Promise> { - const meta = await fetchMeta(); return { id: instance.id, caughtAt: instance.caughtAt.toISOString(), @@ -22,6 +23,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({ isNotResponding: instance.isNotResponding, isSuspended: instance.isSuspended, isBlocked: await shouldBlockInstance(instance.host), + isSilenced: await shouldSilenceInstance(instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts index ed3369bf1..f793d40f6 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/schema/federation-instance.ts @@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = { optional: false, nullable: false, }, + isSilenced: { + type: "boolean", + optional: false, + nullable: false, + }, softwareName: { type: "string", optional: false, diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 29ac726ef..d47e6f300 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -10,7 +10,7 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import { inbox as processInbox } from "@/queue/index.js"; import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js"; +import { Notes, Users, Emojis, NoteReactions, FollowRequests } from "@/models/index.js"; import type { ILocalUser, User } from "@/models/entities/user.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; @@ -330,7 +330,7 @@ router.get("/likes/:like", async (ctx) => { }); // follow -router.get("/follows/:follower/:followee", async (ctx) => { +router.get("/follows/:follower/:followee", async (ctx: Router.RouterContext) => { const verify = await checkFetch(ctx.req); if (verify !== 200) { ctx.status = verify; @@ -365,4 +365,47 @@ router.get("/follows/:follower/:followee", async (ctx) => { setResponseType(ctx); }); +// follow request +router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => { + const verify = await checkFetch(ctx.req); + if (verify !== 200) { + ctx.status = verify; + return; + } + + const followRequest = await FollowRequests.findOneBy({ + id: ctx.params.followRequestId, + }); + + if (followRequest == null) { + ctx.status = 404; + return; + } + + const [follower, followee] = await Promise.all([ + Users.findOneBy({ + id: followRequest.followerId, + host: IsNull(), + }), + Users.findOneBy({ + id: followRequest.followeeId, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + ctx.status = 404; + return; + } + + const meta = await fetchMeta(); + if (meta.secureMode || meta.privateMode) { + ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); + } else { + ctx.set("Cache-Control", "public, max-age=180"); + } + ctx.body = renderActivity(renderFollow(follower, followee)); + setResponseType(ctx); +}); + export default router; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts index 15ad1f9a1..a7b6e95c2 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts @@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => { set.deeplIsPro = config.deepl.isPro; } } + if ( + config.libreTranslate.managed != null && + config.libreTranslate.managed === true + ) { + if (typeof config.libreTranslate.apiUrl === "string") { + set.libreTranslateApiUrl = config.libreTranslate.apiUrl; + } + if (typeof config.libreTranslate.apiKey === "string") { + set.libreTranslateApiKey = config.libreTranslate.apiKey; + } + } if (config.email.managed != null && config.email.managed === true) { set.enableEmail = true; if (typeof config.email.address === "string") { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index c8c639f50..89928af11 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -259,6 +259,16 @@ export const meta = { nullable: false, }, }, + silencedHosts: { + type: "array", + optional: true, + nullable: false, + items: { + type: "string", + optional: false, + nullable: false, + }, + }, allowedHosts: { type: "array", optional: true, @@ -512,7 +522,8 @@ export default define(meta, paramDef, async (ps, me) => { enableGithubIntegration: instance.enableGithubIntegration, enableDiscordIntegration: instance.enableDiscordIntegration, enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: + instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null, pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, @@ -523,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => { customSplashIcons: instance.customSplashIcons, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, + silencedHosts: instance.silencedHosts, allowedHosts: instance.allowedHosts, privateMode: instance.privateMode, secureMode: instance.secureMode, @@ -564,6 +576,8 @@ export default define(meta, paramDef, async (ps, me) => { objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, deeplAuthKey: instance.deeplAuthKey, deeplIsPro: instance.deeplIsPro, + libreTranslateApiUrl: instance.libreTranslateApiUrl, + libreTranslateApiKey: instance.libreTranslateApiKey, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, }; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index f7e79b64b..7f92e5e29 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -61,6 +61,13 @@ export const paramDef = { type: "string", }, }, + silencedHosts: { + type: "array", + nullable: true, + items: { + type: "string", + }, + }, allowedHosts: { type: "array", nullable: true, @@ -124,6 +131,8 @@ export const paramDef = { summalyProxy: { type: "string", nullable: true }, deeplAuthKey: { type: "string", nullable: true }, deeplIsPro: { type: "boolean" }, + libreTranslateApiUrl: { type: "string", nullable: true }, + libreTranslateApiKey: { type: "string", nullable: true }, enableTwitterIntegration: { type: "boolean" }, twitterConsumerKey: { type: "string", nullable: true }, twitterConsumerSecret: { type: "string", nullable: true }, @@ -217,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => { }); } + if (Array.isArray(ps.silencedHosts)) { + let lastValue = ""; + set.silencedHosts = ps.silencedHosts.sort().filter((h) => { + const lv = lastValue; + lastValue = h; + return h !== "" && h !== lv; + }); + } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } @@ -515,6 +533,22 @@ export default define(meta, paramDef, async (ps, me) => { set.deeplIsPro = ps.deeplIsPro; } + if (ps.libreTranslateApiUrl !== undefined) { + if (ps.libreTranslateApiUrl === "") { + set.libreTranslateApiUrl = null; + } else { + set.libreTranslateApiUrl = ps.libreTranslateApiUrl; + } + } + + if (ps.libreTranslateApiKey !== undefined) { + if (ps.libreTranslateApiKey === "") { + set.libreTranslateApiKey = null; + } else { + set.libreTranslateApiKey = ps.libreTranslateApiKey; + } + } + if (ps.enableIpLogging !== undefined) { set.enableIpLogging = ps.enableIpLogging; } diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 8f6184b19..646f38282 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -34,6 +34,7 @@ export const paramDef = { notResponding: { type: "boolean", nullable: true }, suspended: { type: "boolean", nullable: true }, federating: { type: "boolean", nullable: true }, + silenced: { type: "boolean", nullable: true }, subscribing: { type: "boolean", nullable: true }, publishing: { type: "boolean", nullable: true }, limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, @@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => { } } + if (typeof ps.silenced === "boolean") { + const meta = await fetchMeta(true); + if (ps.silenced) { + if (meta.silencedHosts.length === 0) { + return []; + } + query.andWhere("instance.host IN (:...silences)", { + silences: meta.silencedHosts, + }); + } else if (meta.silencedHosts.length > 0) { + query.andWhere("instance.host NOT IN (:...silences)", { + silences: meta.silencedHosts, + }); + } + } + if (typeof ps.notResponding === "boolean") { if (ps.notResponding) { query.andWhere("instance.isNotResponding = TRUE"); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 4dc1c941e..23989750f 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => { enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: + instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null, defaultReaction: instance.defaultReaction, ...(ps.detail diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index c6415ceef..d86fc12a2 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => { const instance = await fetchMeta(); - if (instance.deeplAuthKey == null) { + if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) { return 204; // TODO: 良い感じのエラー返す } let targetLang = ps.targetLang; if (targetLang.includes("-")) targetLang = targetLang.split("-")[0]; + if (instance.libreTranslateApiUrl != null) { + const jsonBody = { + q: note.text, + source: "auto", + target: targetLang, + format: "text", + api_key: instance.libreTranslateApiKey ?? "", + }; + + const url = new URL(instance.libreTranslateApiUrl); + if (url.pathname.endsWith("/")) { + url.pathname = url.pathname.slice(0, -1); + } + if (!url.pathname.endsWith("/translate")) { + url.pathname += "/translate"; + } + const res = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonBody), + agent: getAgentByUrl, + }); + + const json = (await res.json()) as { + detectedLanguage?: { + confidence: number; + language: string; + }; + translatedText: string; + }; + + return { + sourceLang: json.detectedLanguage?.language, + text: json.translatedText, + }; + } + const params = new URLSearchParams(); - params.append("auth_key", instance.deeplAuthKey); + params.append("auth_key", instance.deeplAuthKey ?? ""); params.append("text", note.text); params.append("target_lang", targetLang); diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 06b3ea4ef..3568a27b2 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -29,6 +29,7 @@ import { convertId, IdConvertType as IdType, } from "../../../native-utils/built/index.js"; +import { convertAttachment } from "./mastodon/converters.js"; // re-export native rust id conversion (function and enum) export { IdType, convertId }; @@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => { return; } const data = await client.uploadMedia(multipartData); - ctx.body = data.data; + ctx.body = convertAttachment(data.data as Entity.Attachment); } catch (e: any) { console.error(e); ctx.status = 401; @@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => { return; } const data = await client.uploadMedia(multipartData); - ctx.body = data.data; + ctx.body = convertAttachment(data.data as Entity.Attachment); } catch (e: any) { console.error(e); ctx.status = 401; diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index e8dfe5281..0c59b38f4 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js"; import { apiNotificationsMastodon } from "./endpoints/notifications.js"; import { apiSearchMastodon } from "./endpoints/search.js"; import { getInstance } from "./endpoints/meta.js"; +import { convertAnnouncement, convertFilter } from "./converters.js"; +import { convertId, IdType } from "../index.js"; export function getClient( BASE_URL: string, @@ -68,7 +70,7 @@ export function apiMastodonCompatible(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.getInstanceAnnouncements(); - ctx.body = data.data; + ctx.body = data.data.map(announcement => convertAnnouncement(announcement)); } catch (e: any) { console.error(e); ctx.status = 401; @@ -83,7 +85,9 @@ export function apiMastodonCompatible(router: Router): void { const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.dismissInstanceAnnouncement(ctx.params.id); + const data = await client.dismissInstanceAnnouncement( + convertId(ctx.params.id, IdType.CalckeyId), + ); ctx.body = data.data; } catch (e: any) { console.error(e); @@ -100,7 +104,7 @@ export function apiMastodonCompatible(router: Router): void { // displayed without being logged in try { const data = await client.getFilters(); - ctx.body = data.data; + ctx.body = data.data.map(filter => convertFilter(filter)); } catch (e: any) { console.error(e); ctx.status = 401; diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts new file mode 100644 index 000000000..d9a4f90ef --- /dev/null +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -0,0 +1,44 @@ +import { Entity } from "@calckey/megalodon"; +import { convertId, IdType } from "../index.js"; + +function simpleConvert(data: any) { + data.id = convertId(data.id, IdType.MastodonId); + return data; +} + +export function convertAccount(account: Entity.Account) { return simpleConvert(account); } +export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); } +export function convertAttachment(attachment: Entity.Attachment) { return simpleConvert(attachment); } +export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); } +export function convertList(list: Entity.List) { return simpleConvert(list); } + +export function convertNotification(notification: Entity.Notification) { + notification.account = convertAccount(notification.account); + notification.id = convertId(notification.id, IdType.MastodonId); + if (notification.status) + notification.status = convertStatus(notification.status); + return notification; +} + +export function convertPoll(poll: Entity.Poll) { return simpleConvert(poll); } +export function convertRelationship(relationship: Entity.Relationship) { return simpleConvert(relationship); } + +export function convertStatus(status: Entity.Status) { + status.account = convertAccount(status.account); + status.id = convertId(status.id, IdType.MastodonId); + if (status.in_reply_to_account_id) + status.in_reply_to_account_id = convertId(status.in_reply_to_account_id, IdType.MastodonId); + if (status.in_reply_to_id) + status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId); + status.media_attachments = status.media_attachments.map(attachment => convertAttachment(attachment)); + status.mentions = status.mentions.map(mention => ({ + ...mention, + id: convertId(mention.id, IdType.MastodonId), + })); + if (status.poll) + status.poll = convertPoll(status.poll); + if (status.reblog) + status.reblog = convertStatus(status.reblog); + + return status; +} diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 70bdb74f3..2984c20e3 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -3,8 +3,9 @@ import { resolveUser } from "@/remote/resolve-user.js"; import Router from "@koa/router"; import { FindOptionsWhere, IsNull } from "typeorm"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { argsToBools, limitToInt } from "./timeline.js"; +import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertId, IdType } from "../../index.js"; +import { convertAccount, convertList, convertRelationship, convertStatus } from "../converters.js"; const relationshipModel = { id: "", @@ -62,9 +63,7 @@ export function apiAccountMastodon(router: Router): void { const data = await client.updateCredentials( (ctx.request as any).body as any, ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; + ctx.body = convertAccount(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -81,9 +80,7 @@ export function apiAccountMastodon(router: Router): void { (ctx.request.query as any).acct, "accounts", ); - let resp = data.data.accounts[0]; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; + ctx.body = convertAccount(data.data.accounts[0]); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -91,255 +88,6 @@ export function apiAccountMastodon(router: Router): void { ctx.body = e.response.data; } }); - router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const calcId = convertId(ctx.params.id, IdType.CalckeyId); - const data = await client.getAccount(calcId); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/statuses", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountStatuses( - convertId(ctx.params.id, IdType.CalckeyId), - argsToBools(limitToInt(ctx.query as any)), - ); - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/followers", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowers( - convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), - ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/following", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowing( - convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), - ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/lists", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountLists(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/follow", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.followAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let acct = data.data; - acct.following = true; - acct.id = convertId(acct.id, IdType.MastodonId); - ctx.body = acct; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unfollow", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unfollowAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let acct = data.data; - acct.id = convertId(acct.id, IdType.MastodonId); - acct.following = false; - ctx.body = acct; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/block", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.blockAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unblock", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unblockAccount( - convertId(ctx.params.id, IdType.MastodonId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/mute", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.muteAccount( - convertId(ctx.params.id, IdType.CalckeyId), - (ctx.request as any).body as any, - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unmute", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unmuteAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); router.get("/v1/accounts/relationships", async (ctx) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; @@ -364,11 +112,7 @@ export function apiAccountMastodon(router: Router): void { } const data = await client.getRelationships(reqIds); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; + ctx.body = data.data.map(relationship => convertRelationship(relationship)); } catch (e: any) { console.error(e); let data = e.response.data; @@ -378,33 +122,228 @@ export function apiAccountMastodon(router: Router): void { ctx.body = data; } }); + router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const calcId = convertId(ctx.params.id, IdType.CalckeyId); + const data = await client.getAccount(calcId); + ctx.body = convertAccount(data.data); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/statuses", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountStatuses( + convertId(ctx.params.id, IdType.CalckeyId), + convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))), + ); + ctx.body = data.data.map(status => convertStatus(status)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/followers", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFollowers( + convertId(ctx.params.id, IdType.CalckeyId), + convertTimelinesArgsId(limitToInt(ctx.query as any)), + ); + ctx.body = data.data.map(account => convertAccount(account)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/following", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFollowing( + convertId(ctx.params.id, IdType.CalckeyId), + convertTimelinesArgsId(limitToInt(ctx.query as any)), + ); + ctx.body = data.data.map(account => convertAccount(account)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/lists", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountLists( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = data.data.map(list => convertList(list)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/follow", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.followAccount( + convertId(ctx.params.id, IdType.CalckeyId), + ); + let acct = convertRelationship(data.data); + acct.following = true; + ctx.body = acct; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unfollow", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unfollowAccount( + convertId(ctx.params.id, IdType.CalckeyId), + ); + let acct = convertRelationship(data.data); + acct.following = false; + ctx.body = acct; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/block", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.blockAccount( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = convertRelationship(data.data); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unblock", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unblockAccount( + convertId(ctx.params.id, IdType.MastodonId), + ); + ctx.body = convertRelationship(data.data); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/mute", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.muteAccount( + convertId(ctx.params.id, IdType.CalckeyId), + (ctx.request as any).body as any, + ); + ctx.body = convertRelationship(data.data); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unmute", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unmuteAccount( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = convertRelationship(data.data); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); router.get("/v1/bookmarks", async (ctx) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { const data = (await client.getBookmarks( - limitToInt(ctx.query as any), - )) as any; - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; + convertTimelinesArgsId(limitToInt(ctx.query as any)), + )); + ctx.body = data.data.map(status => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -417,26 +356,8 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getFavourites(limitToInt(ctx.query as any)); - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; + const data = await client.getFavourites(convertTimelinesArgsId(limitToInt(ctx.query as any))); + ctx.body = data.data.map(status => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -449,12 +370,8 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getMutes(limitToInt(ctx.query as any)); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; + const data = await client.getMutes(convertTimelinesArgsId(limitToInt(ctx.query as any))); + ctx.body = data.data.map(account => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -467,12 +384,8 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getBlocks(limitToInt(ctx.query as any)); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; + const data = await client.getBlocks(convertTimelinesArgsId(limitToInt(ctx.query as any))); + ctx.body = data.data.map(account => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -488,11 +401,7 @@ export function apiAccountMastodon(router: Router): void { const data = await client.getFollowRequests( ((ctx.query as any) || { limit: 20 }).limit, ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; + ctx.body = data.data.map(account => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -510,9 +419,7 @@ export function apiAccountMastodon(router: Router): void { const data = await client.acceptFollowRequest( convertId(ctx.params.id, IdType.CalckeyId), ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -531,9 +438,7 @@ export function apiAccountMastodon(router: Router): void { const data = await client.rejectFollowRequest( convertId(ctx.params.id, IdType.CalckeyId), ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index d21bc1d33..7343fc337 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,6 +1,8 @@ import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; +import { IdType, convertId } from "../../index.js"; +import { convertFilter } from "../converters.js"; export function apiFilterMastodon(router: Router): void { router.get("/v1/filters", async (ctx) => { @@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void { const body: any = ctx.request.body; try { const data = await client.getFilters(); - ctx.body = data.data; + ctx.body = data.data.map(filter => convertFilter(filter)); } catch (e: any) { console.error(e); ctx.status = 401; @@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const data = await client.getFilter(ctx.params.id); - ctx.body = data.data; + const data = await client.getFilter( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertFilter(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void { const body: any = ctx.request.body; try { const data = await client.createFilter(body.phrase, body.context, body); - ctx.body = data.data; + ctx.body = convertFilter(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void { const body: any = ctx.request.body; try { const data = await client.updateFilter( - ctx.params.id, + convertId(ctx.params.id, IdType.CalckeyId), body.phrase, body.context, ); - ctx.body = data.data; + ctx.body = convertFilter(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const data = await client.deleteFilter(ctx.params.id); + const data = await client.deleteFilter( + convertId(ctx.params.id, IdType.CalckeyId) + ); ctx.body = data.data; } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 8508f1d48..868377b78 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,8 +1,10 @@ import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import Router from "@koa/router"; import { koaBody } from "koa-body"; +import { convertId, IdType } from "../../index.js"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { toTextWithReaction } from "./timeline.js"; +import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js"; +import { convertNotification } from "../converters.js"; function toLimitToInt(q: any) { if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); return q; @@ -15,9 +17,10 @@ export function apiNotificationsMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const data = await client.getNotifications(toLimitToInt(ctx.query)); + const data = await client.getNotifications(convertTimelinesArgsId(toLimitToInt(ctx.query))); const notfs = data.data; const ret = notfs.map((n) => { + n = convertNotification(n); if (n.type !== "follow" && n.type !== "follow_request") { if (n.type === "reaction") n.type = "favourite"; n.status = toTextWithReaction( @@ -43,8 +46,10 @@ export function apiNotificationsMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const dataRaw = await client.getNotification(ctx.params.id); - const data = dataRaw.data; + const dataRaw = await client.getNotification( + convertId(ctx.params.id, IdType.CalckeyId) + ); + const data = convertNotification(dataRaw.data); if (data.type !== "follow" && data.type !== "follow_request") { if (data.type === "reaction") data.type = "favourite"; ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; @@ -79,7 +84,9 @@ export function apiNotificationsMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const data = await client.dismissNotification(ctx.params.id); + const data = await client.dismissNotification( + convertId(ctx.params.id, IdType.CalckeyId) + ); ctx.body = data.data; } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index e4990811a..98349cfd2 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -3,7 +3,8 @@ import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import axios from "axios"; import { Converter } from "@calckey/megalodon"; -import { limitToInt } from "./timeline.js"; +import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; +import { convertAccount, convertStatus } from "../converters.js"; export function apiSearchMastodon(router: Router): void { router.get("/v1/search", async (ctx) => { @@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const query: any = limitToInt(ctx.query); + const query: any = convertTimelinesArgsId(limitToInt(ctx.query)); const type = query.type || ""; const data = await client.search(query.q, type, query); ctx.body = data.data; @@ -27,18 +28,18 @@ export function apiSearchMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const query: any = limitToInt(ctx.query); + const query: any = convertTimelinesArgsId(limitToInt(ctx.query)); const type = query.type; if (type) { const data = await client.search(query.q, type, query); - ctx.body = data.data; + ctx.body = data.data.accounts.map(account => convertAccount(account)); } else { const acct = await client.search(query.q, "accounts", query); const stat = await client.search(query.q, "statuses", query); const tags = await client.search(query.q, "hashtags", query); ctx.body = { - accounts: acct.data.accounts, - statuses: stat.data.statuses, + accounts: acct.data.accounts.map(account => convertAccount(account)), + statuses: stat.data.statuses.map(status => convertStatus(status)), hashtags: tags.data.hashtags, }; } @@ -57,7 +58,7 @@ export function apiSearchMastodon(router: Router): void { ctx.request.hostname, accessTokens, ); - ctx.body = data; + ctx.body = data.map(status => convertStatus(status)); } catch (e: any) { console.error(e); ctx.status = 401; @@ -69,12 +70,16 @@ export function apiSearchMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; try { const query: any = ctx.query; - const data = await getFeaturedUser( + let data = await getFeaturedUser( BASE_URL, ctx.request.hostname, accessTokens, query.limit || 20, ); + data = data.map(suggestion => { + suggestion.account = convertAccount(suggestion.account); + return suggestion; + }); console.log(data); ctx.body = data; } catch (e: any) { diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index f7589569c..27b30d113 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -4,7 +4,9 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import axios from "axios"; import querystring from "node:querystring"; import qs from "qs"; -import { limitToInt } from "./timeline.js"; +import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; +import { convertId, IdType } from "../../index.js"; +import { convertAccount, convertAttachment, convertPoll, convertStatus } from "../converters.js"; function normalizeQuery(data: any) { const str = querystring.stringify(data); @@ -18,6 +20,8 @@ export function apiStatusMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { let body: any = ctx.request.body; + if (body.in_reply_to_id) + body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId); if ( (!body.poll && body["poll[options][]"]) || (!body.media_ids && body["media_ids[]"]) @@ -54,7 +58,7 @@ export function apiStatusMastodon(router: Router): void { body.sensitive = typeof sensitive === "string" ? sensitive === "true" : sensitive; const data = await client.postStatus(text, body); - ctx.body = data.data; + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -66,8 +70,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.getStatus( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -79,7 +85,9 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteStatus(ctx.params.id); + const data = await client.deleteStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); ctx.body = data.data; } catch (e: any) { console.error(e.response.data, request.params.id); @@ -100,10 +108,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const id = ctx.params.id; + const id = convertId(ctx.params.id, IdType.CalckeyId); const data = await client.getStatusContext( id, - limitToInt(ctx.query as any), + convertTimelinesArgsId(limitToInt(ctx.query as any)), ); const status = await client.getStatus(id); let reqInstance = axios.create({ @@ -126,6 +134,8 @@ export function apiStatusMastodon(router: Router): void { text, ), ); + data.data.ancestors = data.data.ancestors.map(status => convertStatus(status)); + data.data.descendants = data.data.descendants.map(status => convertStatus(status)); ctx.body = data.data; } catch (e: any) { console.error(e); @@ -141,8 +151,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatusRebloggedBy(ctx.params.id); - ctx.body = data.data; + const data = await client.getStatusRebloggedBy( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = data.data.map(account => convertAccount(account)); } catch (e: any) { console.error(e); ctx.status = 401; @@ -165,11 +177,11 @@ export function apiStatusMastodon(router: Router): void { const react = await getFirstReaction(BASE_URL, accessTokens); try { const a = (await client.createEmojiReaction( - ctx.params.id, + convertId(ctx.params.id, IdType.CalckeyId), react, )) as any; //const data = await client.favouriteStatus(ctx.params.id) as any; - ctx.body = a.data; + ctx.body = convertStatus(a.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -186,8 +198,11 @@ export function apiStatusMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); const react = await getFirstReaction(BASE_URL, accessTokens); try { - const data = await client.deleteEmojiReaction(ctx.params.id, react); - ctx.body = data.data; + const data = await client.deleteEmojiReaction( + convertId(ctx.params.id, IdType.CalckeyId), + react + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -203,8 +218,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.reblogStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.reblogStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -220,8 +237,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unreblogStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.unreblogStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -237,8 +256,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.bookmarkStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.bookmarkStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -254,8 +275,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = (await client.unbookmarkStatus(ctx.params.id)) as any; - ctx.body = data.data; + const data = await client.unbookmarkStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -271,8 +294,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.pinStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.pinStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -288,8 +313,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unpinStatus(ctx.params.id); - ctx.body = data.data; + const data = await client.unpinStatus( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -302,8 +329,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getMedia(ctx.params.id); - ctx.body = data.data; + const data = await client.getMedia( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertAttachment(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -316,10 +345,10 @@ export function apiStatusMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.updateMedia( - ctx.params.id, + convertId(ctx.params.id, IdType.CalckeyId), ctx.request.body as any, ); - ctx.body = data.data; + ctx.body = convertAttachment(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -331,8 +360,10 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getPoll(ctx.params.id); - ctx.body = data.data; + const data = await client.getPoll( + convertId(ctx.params.id, IdType.CalckeyId) + ); + ctx.body = convertPoll(data.data); } catch (e: any) { console.error(e); ctx.status = 401; @@ -347,10 +378,10 @@ export function apiStatusMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.votePoll( - ctx.params.id, + convertId(ctx.params.id, IdType.CalckeyId), (ctx.request.body as any).choices, ); - ctx.body = data.data; + ctx.body = convertPoll(data.data); } catch (e: any) { console.error(e); ctx.status = 401; diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 57e5d9bb0..268c6a161 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js"; import { statusModel } from "./status.js"; import Autolinker from "autolinker"; import { ParsedUrlQuery } from "querystring"; +import { convertAccount, convertList, convertStatus } from "../converters.js"; +import { convertId, IdType } from "../../index.js"; export function limitToInt(q: ParsedUrlQuery) { let object: any = q; @@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) { return q; } +export function convertTimelinesArgsId(q: ParsedUrlQuery) { + if (typeof q.min_id === "string") + q.min_id = convertId(q.min_id, IdType.CalckeyId); + if (typeof q.max_id === "string") + q.max_id = convertId(q.max_id, IdType.CalckeyId); + if (typeof q.since_id === "string") + q.since_id = convertId(q.since_id, IdType.CalckeyId); + return q; +} + export function toTextWithReaction(status: Entity.Status[], host: string) { return status.map((t) => { if (!t) return statusModel(null, null, [], "no content"); @@ -97,9 +109,10 @@ export function apiTimelineMastodon(router: Router): void { try { const query: any = ctx.query; const data = query.local - ? await client.getLocalTimeline(argsToBools(limitToInt(query))) - : await client.getPublicTimeline(argsToBools(limitToInt(query))); - ctx.body = toTextWithReaction(data.data, ctx.hostname); + ? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query)))) + : await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query)))); + let resp = data.data.map(status => convertStatus(status)); + ctx.body = toTextWithReaction(resp, ctx.hostname); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -116,9 +129,10 @@ export function apiTimelineMastodon(router: Router): void { try { const data = await client.getTagTimeline( ctx.params.hashtag, - argsToBools(limitToInt(ctx.query)), + convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))), ); - ctx.body = toTextWithReaction(data.data, ctx.hostname); + let resp = data.data.map(status => convertStatus(status)); + ctx.body = toTextWithReaction(resp, ctx.hostname); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -132,8 +146,9 @@ export function apiTimelineMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getHomeTimeline(limitToInt(ctx.query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); + const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(ctx.query))); + let resp = data.data.map(status => convertStatus(status)); + ctx.body = toTextWithReaction(resp, ctx.hostname); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -149,10 +164,11 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.getListTimeline( - ctx.params.listId, - limitToInt(ctx.query), + convertId(ctx.params.listId, IdType.CalckeyId), + convertTimelinesArgsId(limitToInt(ctx.query)), ); - ctx.body = toTextWithReaction(data.data, ctx.hostname); + let resp = data.data.map(status => convertStatus(status)); + ctx.body = toTextWithReaction(resp, ctx.hostname); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -166,7 +182,7 @@ export function apiTimelineMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getConversationTimeline(limitToInt(ctx.query)); + const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(ctx.query))); ctx.body = data.data; } catch (e: any) { console.error(e); @@ -181,7 +197,7 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.getLists(); - ctx.body = data.data; + ctx.body = data.data.map(list => convertList(list)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -196,8 +212,10 @@ export function apiTimelineMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getList(ctx.params.id); - ctx.body = data.data; + const data = await client.getList( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = convertList(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -212,7 +230,7 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.createList((ctx.request.body as any).title); - ctx.body = data.data; + ctx.body = convertList(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -227,8 +245,11 @@ export function apiTimelineMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title); - ctx.body = data.data; + const data = await client.updateList( + convertId(ctx.params.id, IdType.CalckeyId), + (ctx.request.body as any).title + ); + ctx.body = convertList(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -244,7 +265,9 @@ export function apiTimelineMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteList(ctx.params.id); + const data = await client.deleteList( + convertId(ctx.params.id, IdType.CalckeyId), + ); ctx.body = data.data; } catch (e: any) { console.error(e); @@ -262,10 +285,10 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.getAccountsInList( - ctx.params.id, - ctx.query as any, + convertId(ctx.params.id, IdType.CalckeyId), + convertTimelinesArgsId(ctx.query as any), ); - ctx.body = data.data; + ctx.body = data.data.map(account => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -282,8 +305,8 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.addAccountsToList( - ctx.params.id, - (ctx.query as any).account_ids, + convertId(ctx.params.id, IdType.CalckeyId), + (ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)), ); ctx.body = data.data; } catch (e: any) { @@ -302,8 +325,8 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const data = await client.deleteAccountsFromList( - ctx.params.id, - (ctx.query as any).account_ids, + convertId(ctx.params.id, IdType.CalckeyId), + (ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)), ); ctx.body = data.data; } catch (e: any) { diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts index d24a74c12..440d0e368 100644 --- a/packages/backend/src/server/api/private/signup.ts +++ b/packages/backend/src/server/api/private/signup.ts @@ -55,7 +55,7 @@ export default async (ctx: Koa.Context) => { return; } - const available = await validateEmailForAccount(emailAddress); + const { available } = await validateEmailForAccount(emailAddress); if (!available) { ctx.status = 400; return; diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 642a17d57..270026dcc 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -399,28 +399,31 @@ router.get("/notes/:note", async (ctx, next) => { visibility: In(["public", "home"]), }); - if (note) { - const _note = await Notes.pack(note); - const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); - await ctx.render("note", { - note: _note, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: note.userId }), - ), - // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - privateMode: meta.privateMode, - themeColor: meta.themeColor, - }); + try { + if (note) { + const _note = await Notes.pack(note); + + const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); + const meta = await fetchMeta(); + await ctx.render("note", { + note: _note, + profile, + avatarUrl: await Users.getAvatarUrl( + await Users.findOneByOrFail({ id: note.userId }), + ), + // TODO: Let locale changeable by instance setting + summary: getNoteSummary(_note), + instanceName: meta.name || "Calckey", + icon: meta.iconUrl, + privateMode: meta.privateMode, + themeColor: meta.themeColor, + }); - ctx.set("Cache-Control", "public, max-age=15"); + ctx.set("Cache-Control", "public, max-age=15"); - return; - } + return; + } + } catch {} await next(); }); diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index f6545b131..e2dd3fc33 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -6,11 +6,13 @@ import { NoteThreadMutings, UserProfiles, Users, + Followings, } from "@/models/index.js"; import { genId } from "@/misc/gen-id.js"; import type { User } from "@/models/entities/user.js"; import type { Notification } from "@/models/entities/notification.js"; import { sendEmailNotification } from "./send-email-notification.js"; +import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; export async function createNotification( notifieeId: User["id"], @@ -21,6 +23,26 @@ export async function createNotification( return null; } + if ( + data.notifierId && + ["mention", "reply", "renote", "quote", "reaction"].includes(type) + ) { + const notifier = await Users.findOneBy({ id: data.notifierId }); + // suppress if the notifier does not exist or is silenced. + if (!notifier) return null; + + // suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee. + if ( + (notifier.isSilenced || + (Users.isRemoteUser(notifier) && + (await shouldSilenceInstance(notifier.host)))) && + !(await Followings.exist({ + where: { followerId: notifieeId, followeeId: data.notifierId }, + })) + ) + return null; + } + const profile = await UserProfiles.findOneBy({ userId: notifieeId }); const isMuted = profile?.mutingNotificationTypes.includes(type); diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts index 61a8c6b26..3a77676b3 100644 --- a/packages/backend/src/services/following/create.ts +++ b/packages/backend/src/services/following/create.ts @@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js import type { Packed } from "@/misc/schema.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { webhookDeliver } from "@/queue/index.js"; +import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; const logger = new Logger("following/create"); @@ -226,13 +227,19 @@ export default async function ( }); // フォロー対象が鍵アカウントである or + // The follower is silenced, or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or - // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである + // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or + // The follower is remote, the followee is local, and the follower is in a silenced instance. // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく if ( followee.isLocked || + follower.isSilenced || (followeeProfile.carefulBot && follower.isBot) || - (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) + (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) || + (Users.isRemoteUser(follower) && + Users.isLocalUser(followee) && + (await shouldSilenceInstance(follower.host))) ) { let autoAccept = false; diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts index 8b2e86ab5..50dbd9b3b 100644 --- a/packages/backend/src/services/following/requests/create.ts +++ b/packages/backend/src/services/following/requests/create.ts @@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js"; import { Blockings, FollowRequests, Users } from "@/models/index.js"; import { genId } from "@/misc/gen-id.js"; import { createNotification } from "../../create-notification.js"; +import config from "@/config/index.js"; export default async function ( follower: { @@ -79,7 +80,13 @@ export default async function ( } if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderFollow(follower, followee)); + const content = renderActivity( + renderFollow( + follower, + followee, + requestId ?? `${config.url}/follows/${followRequest.id}`, + ), + ); deliver(follower, content, followee.inbox); } } diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 5dd324d89..f1164c9c6 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -39,7 +39,7 @@ import { } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { App } from "@/models/entities/app.js"; -import { Not, In } from "typeorm"; +import { Not, In, IsNull } from "typeorm"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import { genId } from "@/misc/gen-id.js"; import { @@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js"; import type { UserProfile } from "@/models/entities/user-profile.js"; import { db } from "@/db/postgre.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js"; +import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; const mutedWordsCache = new Cache< { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] @@ -166,6 +167,7 @@ export default async ( data: Option, silent = false, ) => + // rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME new Promise(async (res, rej) => { // If you reply outside the channel, match the scope of the target. // TODO (I think it's a process that could be done on the client side, but it's server side for now.) @@ -203,6 +205,15 @@ export default async ( data.visibility = "home"; } + // Enforce home visibility if the user is in a silenced instance. + if ( + data.visibility === "public" && + Users.isRemoteUser(user) && + (await shouldSilenceInstance(user.host)) + ) { + data.visibility = "home"; + } + // Reject if the target of the renote is a public range other than "Home or Entire". if ( data.renote && diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index 1a3c52eb5..277393eb4 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -118,7 +118,7 @@ export default async ( userId: user.id, }); - // リアクションされたユーザーがローカルユーザーなら通知を作成 + // Create notification if the reaction target is a local user. if (note.userHost === null) { createNotification(note.userId, "reaction", { notifierId: user.id, @@ -143,7 +143,7 @@ export default async ( } }); - //#region 配信 + //#region deliver if (Users.isLocalUser(user) && !note.localOnly) { const content = renderActivity(await renderLike(record, note)); const dm = new DeliverManager(user, content); diff --git a/packages/calckey-js/.eslintignore b/packages/calckey-js/.eslintignore deleted file mode 100644 index f22128f04..000000000 --- a/packages/calckey-js/.eslintignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -/built -/coverage -/.eslintrc.js -/jest.config.ts -/test -/test-d diff --git a/packages/calckey-js/.eslintrc.js b/packages/calckey-js/.eslintrc.js deleted file mode 100644 index 164cf1fbe..000000000 --- a/packages/calckey-js/.eslintrc.js +++ /dev/null @@ -1,65 +0,0 @@ -module.exports = { - root: true, - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["@typescript-eslint"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - rules: { - indent: [ - "error", - "tab", - { - SwitchCase: 1, - MemberExpression: "off", - flatTernaryExpressions: true, - ArrayExpression: "first", - ObjectExpression: "first", - }, - ], - "eol-last": ["error", "always"], - semi: ["error", "always"], - quotes: ["error", "single"], - "comma-dangle": ["error", "always-multiline"], - "keyword-spacing": [ - "error", - { - before: true, - after: true, - }, - ], - "key-spacing": [ - "error", - { - beforeColon: false, - afterColon: true, - }, - ], - "space-infix-ops": ["error"], - "space-before-blocks": ["error", "always"], - "object-curly-spacing": ["error", "always"], - "nonblock-statement-body-position": ["error", "beside"], - eqeqeq: ["error", "always", { null: "ignore" }], - "no-multiple-empty-lines": ["error", { max: 1 }], - "no-multi-spaces": ["error"], - "no-var": ["error"], - "prefer-arrow-callback": ["error"], - "no-throw-literal": ["error"], - "no-param-reassign": ["warn"], - "no-constant-condition": ["warn"], - "no-empty-pattern": ["warn"], - "@typescript-eslint/no-unnecessary-condition": ["error"], - "@typescript-eslint/no-inferrable-types": ["warn"], - "@typescript-eslint/no-non-null-assertion": ["warn"], - "@typescript-eslint/explicit-function-return-type": ["warn"], - "@typescript-eslint/no-misused-promises": [ - "error", - { - checksVoidReturn: false, - }, - ], - "@typescript-eslint/consistent-type-imports": "error", - }, -}; diff --git a/packages/calckey-js/package.json b/packages/calckey-js/package.json index d68f24175..598dd1cdb 100644 --- a/packages/calckey-js/package.json +++ b/packages/calckey-js/package.json @@ -9,9 +9,8 @@ "tsd": "tsd", "api": "pnpm api-extractor run --local --verbose", "api-prod": "pnpm api-extractor run --verbose", - "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "typecheck": "tsc --noEmit", - "lint": "pnpm typecheck && pnpm eslint", + "lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"", "jest": "jest --coverage --detectOpenHandles", "test": "pnpm jest && pnpm tsd" }, diff --git a/packages/calckey-js/src/api.types.ts b/packages/calckey-js/src/api.types.ts index bef00da4e..478b86721 100644 --- a/packages/calckey-js/src/api.types.ts +++ b/packages/calckey-js/src/api.types.ts @@ -55,6 +55,7 @@ export type Endpoints = { "admin/get-table-stats": { req: TODO; res: TODO }; "admin/invite": { req: TODO; res: TODO }; "admin/logs": { req: TODO; res: TODO }; + "admin/meta": { req: TODO; res: TODO }; "admin/reset-password": { req: TODO; res: TODO }; "admin/resolve-abuse-user-report": { req: TODO; res: TODO }; "admin/resync-chart": { req: TODO; res: TODO }; diff --git a/packages/client/package.json b/packages/client/package.json index 49c175b15..173585503 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,7 +32,7 @@ "autosize": "5.0.2", "blurhash": "1.1.5", "broadcast-channel": "4.19.1", - "browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git", + "browser-image-resizer": "github:misskey-dev/browser-image-resizer", "calckey-js": "workspace:*", "chart.js": "4.1.1", "chartjs-adapter-date-fns": "2.0.1", diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue index 5f1a5bdb7..feac281d9 100644 --- a/packages/client/src/components/MkButton.vue +++ b/packages/client/src/components/MkButton.vue @@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void { } &:focus-visible { - outline: solid 2px var(--focus); - outline-offset: 2px; + outline: auto; } &.inline { diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue index 659cb1fbb..5e59853b6 100644 --- a/packages/client/src/components/MkCwButton.vue +++ b/packages/client/src/components/MkCwButton.vue @@ -1,5 +1,6 @@ +
- - + + diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue index 5c3e6b702..99a0ab098 100644 --- a/packages/client/src/ui/classic.header.vue +++ b/packages/client/src/ui/classic.header.vue @@ -83,6 +83,7 @@
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue index b70a3c984..fa72c5765 100644 --- a/packages/client/src/ui/classic.sidebar.vue +++ b/packages/client/src/ui/classic.sidebar.vue @@ -5,7 +5,7 @@ class="item _button account" @click="openAccountMenu" > - @@ -299,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) { width: 46px; height: 46px; padding: 0; + margin-inline: 0 !important; } } @@ -372,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) { > i { width: 32px; + justify-content: center; } > i, diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index a721ffd0b..266effd9a 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -227,6 +227,8 @@ onMounted(() => { } .gbhvwtnk { + display: flex; + justify-content: center; $ui-font-size: 1em; $widgets-hide-threshold: 1200px; diff --git a/patrons.json b/patrons.json index 979fa3d8d..f27fd534a 100644 --- a/patrons.json +++ b/patrons.json @@ -26,6 +26,8 @@ "@jovikowi@calckey.social", "@padraig@calckey.social", "@pancakes@cats.city", + "@theresmiling@calckey.social", + "@AlderForrest@calckey.social", "Interkosmos Link" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf4c29c06..77b0e0d4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,12 @@ importers: '@tensorflow/tfjs': specifier: ^3.21.0 version: 3.21.0(seedrandom@3.0.5) + focus-trap: + specifier: ^7.2.0 + version: 7.2.0 + focus-trap-vue: + specifier: ^4.0.1 + version: 4.0.1(focus-trap@7.2.0)(vue@3.2.47) js-yaml: specifier: 4.1.0 version: 4.1.0 @@ -81,8 +87,8 @@ importers: specifier: ^4.6.4 version: 4.10.2 '@calckey/megalodon': - specifier: 5.1.24 - version: 5.1.24 + specifier: 5.2.0 + version: 5.2.0 '@discordapp/twemoji': specifier: 14.0.2 version: 14.0.2 @@ -720,8 +726,8 @@ importers: specifier: 4.19.1 version: 4.19.1 browser-image-resizer: - specifier: https://github.com/misskey-dev/browser-image-resizer.git - version: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c + specifier: github:misskey-dev/browser-image-resizer + version: github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e calckey-js: specifier: workspace:* version: link:../calckey-js @@ -1065,6 +1071,11 @@ packages: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.21.5: + resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helper-validator-identifier@7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} @@ -1108,6 +1119,14 @@ packages: '@babel/types': 7.21.4 dev: true + /@babel/parser@7.21.5: + resolution: {integrity: sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.21.5 + dev: false + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.4): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -1294,6 +1313,15 @@ packages: to-fast-properties: 2.0.0 dev: true + /@babel/types@7.21.5: + resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.21.5 + '@babel/helper-validator-identifier': 7.19.1 + to-fast-properties: 2.0.0 + dev: false + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -1378,8 +1406,8 @@ packages: '@bull-board/api': 4.10.2 dev: false - /@calckey/megalodon@5.1.24: - resolution: {integrity: sha512-VRd6x8MFQ2pMF0rnGF67/GVxgp/92CV7lg2XT1wnPAfQZ1NTsjwlDQX3HewEW3fSG/r7Nzh5WbIBXC8WMWKs9g==} + /@calckey/megalodon@5.2.0: + resolution: {integrity: sha512-9MEjzKJPyd7o5bHGGlNq4oE1tMt22GUJ8o8tZXcXSpXlrSDb2rSwumirM1KXUWTW8G6NGi1leCM59gOBGLko3w==} engines: {node: '>=15.0.0'} dependencies: '@types/oauth': 0.9.1 @@ -3805,6 +3833,15 @@ packages: source-map: 0.6.1 dev: true + /@vue/compiler-core@3.2.47: + resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==} + dependencies: + '@babel/parser': 7.21.5 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + source-map: 0.6.1 + dev: false + /@vue/compiler-dom@3.2.45: resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==} dependencies: @@ -3812,6 +3849,13 @@ packages: '@vue/shared': 3.2.45 dev: true + /@vue/compiler-dom@3.2.47: + resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} + dependencies: + '@vue/compiler-core': 3.2.47 + '@vue/shared': 3.2.47 + dev: false + /@vue/compiler-sfc@2.7.14: resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} dependencies: @@ -3835,6 +3879,21 @@ packages: source-map: 0.6.1 dev: true + /@vue/compiler-sfc@3.2.47: + resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} + dependencies: + '@babel/parser': 7.21.5 + '@vue/compiler-core': 3.2.47 + '@vue/compiler-dom': 3.2.47 + '@vue/compiler-ssr': 3.2.47 + '@vue/reactivity-transform': 3.2.47 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + magic-string: 0.25.9 + postcss: 8.4.23 + source-map: 0.6.1 + dev: false + /@vue/compiler-ssr@3.2.45: resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==} dependencies: @@ -3842,6 +3901,13 @@ packages: '@vue/shared': 3.2.45 dev: true + /@vue/compiler-ssr@3.2.47: + resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} + dependencies: + '@vue/compiler-dom': 3.2.47 + '@vue/shared': 3.2.47 + dev: false + /@vue/reactivity-transform@3.2.45: resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==} dependencies: @@ -3852,12 +3918,28 @@ packages: magic-string: 0.25.9 dev: true + /@vue/reactivity-transform@3.2.47: + resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} + dependencies: + '@babel/parser': 7.21.5 + '@vue/compiler-core': 3.2.47 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + magic-string: 0.25.9 + dev: false + /@vue/reactivity@3.2.45: resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==} dependencies: '@vue/shared': 3.2.45 dev: true + /@vue/reactivity@3.2.47: + resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==} + dependencies: + '@vue/shared': 3.2.47 + dev: false + /@vue/runtime-core@3.2.45: resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==} dependencies: @@ -3865,6 +3947,13 @@ packages: '@vue/shared': 3.2.45 dev: true + /@vue/runtime-core@3.2.47: + resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==} + dependencies: + '@vue/reactivity': 3.2.47 + '@vue/shared': 3.2.47 + dev: false + /@vue/runtime-dom@3.2.45: resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==} dependencies: @@ -3873,6 +3962,14 @@ packages: csstype: 2.6.21 dev: true + /@vue/runtime-dom@3.2.47: + resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==} + dependencies: + '@vue/runtime-core': 3.2.47 + '@vue/shared': 3.2.47 + csstype: 2.6.21 + dev: false + /@vue/server-renderer@3.2.45(vue@3.2.45): resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==} peerDependencies: @@ -3883,10 +3980,24 @@ packages: vue: 3.2.45 dev: true + /@vue/server-renderer@3.2.47(vue@3.2.47): + resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} + peerDependencies: + vue: 3.2.47 + dependencies: + '@vue/compiler-ssr': 3.2.47 + '@vue/shared': 3.2.47 + vue: 3.2.47 + dev: false + /@vue/shared@3.2.45: resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} dev: true + /@vue/shared@3.2.47: + resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} + dev: false + /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -5914,8 +6025,8 @@ packages: requiresBuild: true dev: false - /core-js@3.30.0: - resolution: {integrity: sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==} + /core-js@3.30.1: + resolution: {integrity: sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==} requiresBuild: true dev: true @@ -6074,7 +6185,6 @@ packages: /csstype@2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} - dev: true /csstype@3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} @@ -6979,7 +7089,6 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -7445,6 +7554,22 @@ packages: readable-stream: 2.3.7 dev: true + /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.47): + resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==} + peerDependencies: + focus-trap: ^7.0.0 + vue: ^3.0.0 + dependencies: + focus-trap: 7.2.0 + vue: 3.2.47 + dev: false + + /focus-trap@7.2.0: + resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==} + dependencies: + tabbable: 6.1.1 + dev: false + /follow-redirects@1.15.2(debug@4.3.4): resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -7596,7 +7721,7 @@ packages: resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==} engines: {node: '>= 0.10'} dependencies: - graceful-fs: 4.2.11 + graceful-fs: 4.2.10 through2: 2.0.5 dev: true @@ -9685,7 +9810,7 @@ packages: /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: - graceful-fs: 4.2.11 + graceful-fs: 4.2.10 /jsonfile@5.0.0: resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} @@ -9699,7 +9824,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.11 + graceful-fs: 4.2.10 dev: true /jsonld@6.0.0: @@ -10350,7 +10475,6 @@ packages: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: sourcemap-codec: 1.4.8 - dev: true /mailcheck@1.1.1: resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} @@ -10823,6 +10947,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + /nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -11944,6 +12074,15 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss@8.4.23: + resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: false + /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -13267,7 +13406,6 @@ packages: /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true /sparkles@1.0.1: resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} @@ -13686,6 +13824,10 @@ packages: resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==} dev: true + /tabbable@6.1.1: + resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==} + dev: false + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -14663,7 +14805,7 @@ packages: dependencies: append-buffer: 1.0.2 convert-source-map: 1.9.0 - graceful-fs: 4.2.11 + graceful-fs: 4.2.10 normalize-path: 2.1.1 now-and-later: 2.0.1 remove-bom-buffer: 3.0.0 @@ -14778,6 +14920,16 @@ packages: '@vue/shared': 3.2.45 dev: true + /vue@3.2.47: + resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} + dependencies: + '@vue/compiler-dom': 3.2.47 + '@vue/compiler-sfc': 3.2.47 + '@vue/runtime-dom': 3.2.47 + '@vue/server-renderer': 3.2.47(vue@3.2.47) + '@vue/shared': 3.2.47 + dev: false + /vuedraggable@4.1.0(vue@3.2.45): resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: @@ -15391,10 +15543,10 @@ packages: resolution: {integrity: sha512-+MLeeUcLTlnzVo5xDn9+LVN9oX4esvgZ7qfZczBN+YVUvZBafIrPPVyG2WdjMWU2Qkb2ZAh2M8lpqf1wIoGqJQ==} dev: false - github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c: - resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c} + github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e: + resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/56f504427ad7f6500e141a6d9f3aee42023d7f3e} name: browser-image-resizer - version: 2.2.1-misskey.3 + version: 2.2.1-misskey.4 dev: true github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658: @@ -15402,7 +15554,7 @@ packages: name: plyr version: 3.7.0 dependencies: - core-js: 3.30.0 + core-js: 3.30.1 custom-event-polyfill: 1.0.7 loadjs: 4.2.0 rangetouch: 2.0.1