diff --git a/.woodpecker/commit.yml b/.woodpecker/commit.yml index 6bb1b2d814..f57fd9d1e6 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/locales/ca-ES.yml b/locales/ca-ES.yml index 24e4b6d7d1..49069a8f21 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -65,8 +65,8 @@ import: "Importar" export: "Exportar" files: "Fitxers" download: "Descarregar" -driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les publicacions\ - \ associades a aquest fitxer adjunt també es suprimiran." +driveFileDeleteConfirm: "Estàs segur que vols esborrar el fitxer \"{nom}\"? S'eliminarà\ + \ de totes les notes que el continguin com a fitxer adjunt." unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?" exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà\ \ al teu Disc un cop completada." @@ -140,52 +140,542 @@ 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. + plainDescription: Desactiva els efectes de tots els MFM continguts en aquest efecte + MFM. + scale: Escala + foreground: Color de primer pla + background: Color de fons + backgroundDescription: Canvia el color de fons del text. + scaleDescription: Escala el contingut en una quantitat especificada. + foregroundDescription: Canvia el color de primer pla del text. + plain: Pla _theme: keys: mention: "Menció" renote: "Impulsar" + fg: Text + navBg: Fons de la barra lateral + navFg: Text de la barra lateral + navHoverFg: Text de la barra lateral (Hover) + hashtag: Etiquetes + mentionMe: Mencions (Jo) + infoBg: Fons de l'informació + infoFg: Text informatiu + toastBg: Fons de notificació + listItemHoverBg: Fons de la llista d'elements (Hover) + driveFolderBg: Fons de la carpeta Disc + wallpaperOverlay: Superposició de fons de pantalla + badge: Distintiu + accentLighten: Accent (Lluminós) + accentDarken: Accent (enfosquit) + fgHighlighted: Text ressaltat + indicator: Indicador + focus: Centrar-se + panel: Panell + navIndicator: Indicador de la barra lateral + accent: Accent + header: Encapçalament + navActive: Text de la barra lateral (Active) + link: Enllaç + modalBg: Fons del modal + divider: Divisor + scrollbarHandle: Mànec de la barra de desplaçament + scrollbarHandleHover: Mànec de la barra de desplaçament (Hover) + dateLabelFg: Text de l'etiqueta de data + infoWarnBg: Fons d'advertència + cwBg: Fons del botó CW + cwFg: Text del botó CW + messageBg: Fons del xat + infoWarnFg: Text d'advertència + bg: Fons + shadow: Ombra + cwHoverBg: Fons del botó CW (Hover) + toastFg: Text de notificació + buttonHoverBg: Fons del botó (Hover) + inputBorder: Vora del camp d'entrada + buttonBg: Fons del botó + description: Descripció + installed: "{name} s'ha instal·lat" + installedThemes: Temes instal·lats + builtinThemes: Temes integrats + alreadyInstalled: Aquest tema ja està instal·lat + invalid: El format d'aquest tema no és vàlid + make: Fes un tema + defaultValue: 'Per defecte: {value}' + color: Color + refProp: Fes referència a una propietat + refConst: Fes referència a una constant + key: Clau + func: Funcions + funcKind: Tipus de funció + argument: Argument + basedProp: Propietat de referència + importInfo: Si introdueixes el codi de tema aquí, podeu importar-lo a l'editor de + temes + inputConstantName: Introdueix un nom per a aquesta constant + addConstant: Afegir una constant + code: Codi del tema + alpha: Opacitat + deleteConstantConfirm: De debò vols esborrar la constant {const}? + manage: Gestionar temes + explore: Explora Temes + darken: Enfosquir + base: Fundament + constant: Constant + lighten: Clar + install: Instal·lar un tema _sfx: note: "Posts" notification: "Notificacions" + antenna: Antenes + channel: Notificacions del canal + noteMy: Nota propia + chat: Xat + chatBg: Fons del xat _2fa: step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:" + alreadyRegistered: Ja heu registrat un dispositiu d'autenticació de dos factors. + registerDevice: Registrar un dispositiu nou + securityKeyInfo: A més de l'autenticació d'empremta digital o PIN, també podeu configurar + l'autenticació mitjançant claus de seguretat de maquinari compatibles amb FIDO2 + per protegir encara més el vostre compte. + step4: A partir d'ara, qualsevol intent d'inici de sessió futur demanarà aquest + token d'inici de sessió. + registerKey: Registra una clau de seguretat + step1: En primer lloc, instal·la una aplicació d'autenticació (com ara {a} o {b}) + al dispositiu. + step2: A continuació, escaneja el codi QR que es mostra en aquesta pantalla. + step3: Introdueix el token que t'ha proporcionat l'aplicació per finalitzar la configuració. _widgets: notifications: "Notificacions" timeline: "Línia de temps" + unixClock: Rellotge UNIX + federation: Federació + instanceCloud: Núvol d'instàncies + trends: Tendència + clock: Rellotge + calendar: Calendari + activity: Activitat + photos: Fotos + rssTicker: Ticker RSS + onlineUsers: Usuaris en línia + memo: Notes adhesives + digitalClock: Rellotge digital + postForm: Formulari de notes + slideshow: Presentació de diapositives + serverMetric: Mètriques del servidor + userList: Llista d'usuaris + rss: Lector d'RSS + jobQueue: Cua de treball + _userList: + chooseList: Selecciona una llista + aiscript: Consola AiScript + button: Botó _cw: show: "Carregar més" + files: '{count} fitxers' + hide: Amaga + chars: '{count} caràcters' _visibility: followers: "Seguidors" + publicDescription: La teva nota serà visible per a tots els usuaris + localOnly: Només Local + specified: Directe + home: Sense llistar + homeDescription: Publica només a la línea de temps local + followersDescription: Fes visible només per als teus seguidors + specifiedDescription: Fer visible només per a usuaris determinats + public: Públic + localOnlyDescription: No és visible per als usuaris remots _profile: username: "Nom d'usuari" + metadataEdit: Editar informació addicional + youCanIncludeHashtags: També pots incloure etiquetes al teu perfil. + metadata: Informació adicional + description: Perfil + metadataLabel: Etiqueta + metadataContent: Contingut + changeAvatar: Canvia l'avatar + changeBanner: Canvia el banner + locationDescription: Si primer introduïu la vostra ciutat, es mostrarà l'hora local + a altres usuaris. + name: Nom + metadataDescription: Fent servir això, podràs mostrar camps d'informació addicionals + al vostre perfil. _exportOrImport: followingList: "Seguint" muteList: "Silencia" blockingList: "Bloqueja" userLists: "Llistes" + excludeMutingUsers: Exclou els usuaris silenciats + allNotes: Totes les notes + excludeInactiveUsers: Exclou usuaris inactius _pages: script: categories: list: "Llistes" + flow: Control de flux + random: Aleatori + value: Valors + fn: Funcions + text: Operacions de text + convert: Transformacions + logical: Operació lògica + operation: Càlcul + comparison: Comparació blocks: _join: arg1: "Llistes" + arg2: Separador _randomPick: arg1: "Llistes" _dailyRandomPick: arg1: "Llistes" _seedRandomPick: arg2: "Llistes" + arg1: Llavor _pick: arg1: "Llistes" + arg2: Posició _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 + text: Text + _strPick: + arg1: Text + arg2: Ubicació de la cadena + strPick: Extreure cadena + strReplace: Cadena de substitució + _strReplace: + arg1: Text + arg3: Substitueix per + arg2: Text a substituir + strReverse: Voltejar text + _strReverse: + arg1: Text + join: Concatenació de textos + pick: Selecciona de la llista + listLen: Obtenir la longitud de la llista + stringToNumber: Text a número + number: Número + _stringToNumber: + arg1: Text + splitStrByLine: Dividir el text per salts de línia + _fn: + slots: Ranures + slots-info: Separa cada ranura amb un salt de línia + arg1: Sortida + aiScriptVar: Variable AiScript + fn: Funció + for: Repetir + _numberToString: + arg1: Número + _DRPWPM: + arg1: Llista de text + numberToString: Número a text + _splitStrByLine: + arg1: Text + ref: Variable + DRPWPM: Tria aleatòriament d'una llista ponderada (Canvis un cop al dia per + a cada usuari) + _for: + arg1: Nombre de vegades a repetir + arg2: Acció + strLen: Longitud del text + multiLineText: Text (multilínia) + _strLen: + arg1: Text + textList: Llista de text + _textList: + info: Separa cada ranura amb un salt de línia types: array: "Llistes" + stringArray: Llista de text + boolean: Bandera + string: Text + number: Número + emptySlot: Ranura buida + enviromentVariables: Variables d'entorn + pageVariables: Variables de pàgina + argVariables: Ranures d'entrada + thereIsEmptySlot: L'espai {slot} està buit! + typeError: L'espai {slot} accepta valors del tipus "{expect}", però el valor proporcionat + és del tipus "{actual}"! + newPage: Crea una pàgina nova + editPage: Edita aquesta pàgina + readPage: S'està veient la font d'aquesta pàgina + created: Pàgina creada correctament + updated: Pàgina editada correctament + invalidNameText: Assegurat que el títol de la pàgina no estigui buit + editThisPage: Edita aquesta pàgina + deleted: Pàgina suprimida correctament + pageSetting: Configuració de la pàgina + nameAlreadyExists: L'URL de la pàgina especificat ja existeix + invalidNameTitle: L'URL de la pàgina especificat no és vàlid + viewPage: Consulta la teva pàgina + like: M'agrada + viewSource: Veure la font + summary: Resum de la pàgina + alignCenter: Centrar elements + hideTitleWhenPinned: Amaga el títol de la pàgina quan estigui fixat al perfil + font: Tipus de lletra + fontSerif: Serif + fontSansSerif: Sans Serif + eyeCatchingImageSet: Estableix una miniatura + eyeCatchingImageRemove: Suprimeix la miniatura + chooseBlock: Afegeix un bloc + selectType: Selecciona un tipus + enterVariableName: Introduïu un nom de variable + blocks: + section: Secció + text: Text + textarea: Àrea de text + image: Imatges + if: Si + _if: + variable: Variable + post: Formulari de notes + _post: + text: Contingut + attachCanvasImage: Adjuntar imatge de llenç + canvasId: ID del llenç + _textInput: + name: Nom de la variable + text: Títol + default: Valor per defecte + textInput: Entrada de text + _textareaInput: + name: Nom de la variable + text: Títol + default: Valor per defecte + textareaInput: Entrada de text multilínia + numberInput: Entrada numèrica + _note: + id: ID de la nota + idDescription: També podeu enganxar l'URL de la nota aquí. + detailed: Vista detallada + switch: Canviar + canvas: Llenç + _canvas: + id: Identificador de llenç + width: Amplada + height: Alçada + note: Nota incrustada + _counter: + name: Nom de la variable + text: Títol + inc: Pas + _button: + text: Títol + colored: De colors + action: Comportament quan es prem el botó + _action: + _dialog: + content: Contingut + resetRandom: Restableix la llavor aleatòria + pushEvent: Envia un esdeveniment + _pushEvent: + event: Nom de l'esdeveniment + message: Missatge que s'ha de mostrar quan s'activa + variable: Variable per enviar + no-variable: Cap + dialog: Mostra un diàleg + callAiScript: Invoca AiScript + _callAiScript: + functionName: Nom de la funció + _switch: + default: Valor per defecte + name: Nom de la variable + text: Títol + counter: Comptador + _numberInput: + name: Nom de la variable + text: Títol + default: Valor per defecte + button: Botó + _radioButton: + name: Nom de la variable + title: Títol + values: Llista d'opcions separades per salts de línia + default: Valor per defecte + radioButton: Elecció + variableNameIsAlreadyUsed: Aquest nom de variable ja està en ús + contentBlocks: Contingut + inputBlocks: Entrada + specialBlocks: Especial + variables: Variables + title: Títol + url: URL de la pàgina + unlike: Elimina m'agrada + my: Les meves pàgines + liked: Pàgines que m'han agradat + content: Bloc de pàgines + featured: Popular + inspector: Inspector + contents: Contingut _notification: youWereFollowed: "t'ha seguit" _types: @@ -194,15 +684,61 @@ _notification: renote: "Impulsos" quote: "Citar" reaction: "Reaccions" + all: Tots + reply: Respostes + pollEnded: S'acaben les enquestes + receiveFollowRequest: S'han rebut peticions de seguiment + followRequestAccepted: Sol·licituds de seguiment acceptades + groupInvited: Invitacions per a grups + app: Notificacions d'aplicacions enllaçades + pollVote: Votacions a les enquestes _actions: reply: "Respondre" renote: "Impulsos" + followBack: et va seguir de tornada + youGotQuote: "{name} t'ha citat" + fileUploaded: El fitxer s'ha penjat correctament + youGotMention: "{nom} t'ha esmentat" + youGotReply: "{name} t'ha respost" + youRenoted: Impuls de {name} + youGotPoll: '{name} ha votat a la teva enquesta' + youGotMessagingMessageFromUser: "{name} t'ha enviat un missatge de xat" + youGotMessagingMessageFromGroup: S'ha enviat un missatge de xat al grup {name} + youReceivedFollowRequest: Has rebut una sol·licitud de seguiment + yourFollowRequestAccepted: S'ha acceptat la vostra sol·licitud de seguiment + pollEnded: Es resultat de la enquesta ja està disponible + emptyPushNotificationMessage: Les notificacions push s'han actualitzat + youWereInvitedToGroup: "{userName} t'ha convidat a un grup" _deck: _columns: notifications: "Notificacions" tl: "Línia de temps" list: "Llistes" mentions: "Mencions" + widgets: Ginys + main: Principal + antenna: Antenes + direct: Missatges directes + alwaysShowMainColumn: Mostra sempre la columna principal + columnAlign: Alinear columnes + introduction: Crea la interfície perfecta per a tu organitzant columnes lliurement! + swapRight: Canvia amb la columna de la dreta + swapUp: Canvia amb la columna de d'alt + swapDown: Canvia amb la columna de sota + stackLeft: Apilar amb la columna de l'esquerra + popRight: Treu a la dreta + profile: Espai de treball + newProfile: Nou espai de treball + deleteProfile: Suprimir l'espai de treball + introduction2: Feu clic al + a la dreta de la pantalla per afegir noves columnes + sempre que vulgueu. + widgetsIntroduction: Selecciona "Editar ginys" al menú de columnes i afegeix un + giny. + addColumn: Afegeix una columna + configureColumn: Configuració de columnes + swapLeft: Canvia amb la columna de l'esquerra + renameProfile: Canvia el nom de l'espai de treball + nameAlreadyExists: Aquest nom d'espai de treball ja existeix. blockConfirm: Estás segur que vols bloquejar aquest compte? unsuspendConfirm: Estás segur que vols treure la suspensió d'aquesta compte? unblockConfirm: Estás segur que vols treure el bloqueig d'aquesta compte? @@ -416,9 +952,10 @@ blockedInstances: Instàncies Bloquejades blockedInstancesDescription: Llista les adreces de les instàncies que vols bloquejar. Les instàncies de la llista no podrán comunicarse amb aquesta instància. hiddenTags: Etiquetes Ocultes -hiddenTagsDescription: 'Llista de les etiquetes (sense el #) que vulguis amagar de - les tendències i el explorador. Les etiquetes amagades es poden descobrir per altres - mitjans.' +hiddenTagsDescription: 'Enumereu els etiquetes (sense el #) de les etiquetes que voleu + ocultar de tendències i explorar. Les etiquetes ocultes encara es poden descobrir + per altres mitjans. Les instàncies bloquejades no es veuen afectades encara que + s''enumeren aquí.' noInstances: No hi han instàncies defaultValueIs: 'Per defecte: {value}' suspended: Suspès @@ -471,7 +1008,7 @@ pinnedClipId: ID del clip que vols fixar hcaptcha: hCaptcha manageAntennas: Gestiona les Antenes name: Nom -notesAndReplies: Articles i respostes +notesAndReplies: Notes i respostes silence: Posa en silenci withFiles: Amb fitxers popularUsers: Usuaris populars @@ -580,7 +1117,7 @@ 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: Deck +deck: Taulell width: Amplada generateAccessToken: Genera un token d'accés medium: Mitja @@ -675,3 +1212,830 @@ 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: Ràtio +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. +_instanceTicker: + none: No mostrar mai + remote: Mostra per a usuaris remots + always: Mostra sempre +_serverDisconnectedBehavior: + nothing: No fer res + quiet: Mostra un avís discret + reload: Torna a carregar automàticament + dialog: Mostra el diàleg d'avís +_channel: + create: Crea un canal + edit: Edita el canal + setBanner: Establir bàner + removeBanner: Suprimeix el bàner + featured: Tendència + owned: Propietari + usersCount: '{n} Participants' + following: Seguit + notesCount: '{n} Notes' +_instanceMute: + instanceMuteDescription: Això silenciarà les notes o els impulsos de les instàncies + indicades, incloses les dels usuaris que responguin a un usuari des d'una instància + silenciada. + title: Amaga les notes de les instàncies de la llista. + instanceMuteDescription2: Separar amb noves línies + heading: Llista d'instàncies que cal silenciar +_ago: + future: Futur + justNow: Ara mateix + minutesAgo: Fa {n}m + hoursAgo: Fa {n}h + daysAgo: Fa {n}d + secondsAgo: Fa {n}s + weeksAgo: Fa {n}s + monthsAgo: Fa {n}me + yearsAgo: Fa {n}a +_time: + second: Segon(s) + minute: Minut(s) + hour: Hora(s) + day: Dia(s) +_tutorial: + step5_4: La línea de temps Local de {icon} és on pots veure notes de tots els altres + usuaris en aquesta instància. + step5_2: La teva instància té activades {timelines} diferents. + step5_3: La línia de temps d'Inici {icon} és on pots veure notes dels comptes que + seguiu i de tots els altres en aquest cas. Si prefereixes que la teva línia de + temps d'inici només mostri notes dels comptes que seguiu, podeu canviar-ho fàcilment + a Configuració! + step5_6: La línia de temps de Recomanats {icon} és on pots veure les notes de les + instàncies que els administradors recomanen. + step5_7: La línia de temps Global {icon} és on pots veure les notes de totes les + altres instàncies connectades. + step6_1: Aleshores, què és aquest lloc? + step6_4: Ara ves, explora i diverteix-te! + step1_2: Anem a fer la configuració. Estaràs en funcionament en un tres i no res! + title: Com utilitzar Calckey + step1_1: Benvingut! + step2_1: En primer lloc, empleneu el vostre perfil. + step4_1: Anem a treure't allà fora. + step5_5: La línea de temps Social {icon} és on només pots veure notes dels comptes + que segueixes. + step6_3: Cada servidor funciona de diferents maneres, i no tots els servidors executen + Calckey. Aquest sí que sí! És una mica complicat, però ho aconseguiràs en poc + temps. + step2_2: Proporcionar informació sobre qui sou facilitarà que altres puguin saber + si volen veure les vostres notes o seguir-vos. + step3_1: Ara toca seguir a algunes persones! + step3_2: "Les teves líneas de temps domèstiques i socials es basen en qui seguiu,\ + \ així que proveu de seguir un parell de comptes per començar.\nFeu clic al cercle\ + \ més situat a la part superior dreta d'un perfil per seguir-los." + step4_2: Per a la vostra primera nota, a algunes persones els agrada fer una nota + de {introduction} o un senzill "Hola món!" + step5_1: Línies de temps, línies de temps a tot arreu! + step6_2: Bé, no només t'has unit a Calckey. T'has unit a un portal al Fediverse, + una xarxa interconnectada de milers de servidors, anomenats "instàncies". +_permissions: + "read:account": Consulta la informació del teu compte + "read:blocks": Consulta la teva llista d'usuaris bloquejats + "write:account": Editar la informació del compte + "read:drive": Accedir als fitxers i carpetes del Disc + "read:messaging": Consulta els teus xats + "write:following": Segueix o deixa de seguir altres comptes + "write:mutes": Editar la teva llista d'usuaris silenciats + "read:notifications": Consulta les teves notificacions + "write:notifications": Gestiona les teves notificacions + "write:user-groups": Editar o suprimir grups d'usuaris + "write:blocks": Editar la llista d'usuaris bloquejats + "write:notes": Redactar o suprimir notes + "write:channels": Editar els teus canals + "read:gallery-likes": Consulta la llista de notes de la galeria que t'agraden + "write:drive": Editar o suprimir fitxers i carpetes del Disc + "read:favorites": Consulta la teva llista d'adreces d'interès + "write:favorites": Editeu la teva llista d'adreces d'interès + "write:messaging": Escriu o suprimeix missatges de xat + "read:mutes": Consulta la teva llista d'usuaris silenciats + "write:reactions": Edita les teves reaccions + "write:votes": Vota en una enquesta + "write:pages": Editeu o suprimeix la teva pàgina + "write:page-likes": Editar les pàgines que t'agraden + "read:user-groups": Consulta els teus grups d'usuaris + "read:channels": Consulta els teus canals + "read:gallery": Consulta la teva galeria + "write:gallery": Edita la teva galeria + "write:gallery-likes": Edita la llista de notes de la galeria que t'agraden + "read:following": Consulta la informació sobre a qui segueixes + "read:reactions": Consulta les teves reaccions + "read:pages": Consulta la teva pàgina + "read:page-likes": Veure les pàgines que t'agraden +_poll: + noOnlyOneChoice: Calen almenys dues opcions + canMultipleVote: Permet seleccionar diverses opcions + expiration: Finalitzar l'enquesta + after: Acaba després... + duration: Durada + votesCount: '{n} vots' + totalVotes: '{n} vots en total' + showResult: Veure resultats + choiceN: Opció {n} + noMore: No es poden afegir més opcions + infinite: Mai + at: Acaba el... + deadlineDate: Data de finalització + deadlineTime: Temps + remainingHours: Queden {h} hora(s) {m} minut(s) + remainingDays: Queden {d} dia(s) {h} hores + remainingMinutes: Queden {m} minut(s) {s} segons + voted: Votat + closed: S'ha acabat + remainingSeconds: Queden {s} segons + vote: Vota +_postForm: + _placeholders: + d: Què vols dir? + e: Comença a escriure... + f: Esperant que escriguis... + b: Què passa al teu voltant? + c: En què penses? + a: Què et portes entre mans? + quotePlaceholder: Cita aquesta nota... + replyPlaceholder: Respon a aquesta nota... + channelPlaceholder: Publica en un canal... +_charts: + federation: Federació + usersIncDec: Diferència en el nombre d'usuaris + apRequest: Sol·licituds + usersTotal: Nombre total d'usuaris + activeUsers: Usuaris actius + notesIncDec: Diferència en el nombre de notes + localNotesIncDec: Diferència en el nombre de notes locals + remoteNotesIncDec: Diferència en el nombre de notes remotes + notesTotal: Nombre total de notes + filesIncDec: Diferència en el nombre de fitxers + filesTotal: Nombre total de fitxers + storageUsageTotal: Ús total d'emmagatzematge + storageUsageIncDec: Diferència en l'ús d'emmagatzematge +_instanceCharts: + requests: Sol·licituds + users: Diferència en el nombre d'usuaris + usersTotal: Nombre acumulat d'usuaris + notes: Diferència en el nombre de notes + ffTotal: Nombre acumulat d'usuaris seguits/seguidors seguits + cacheSize: Diferència en la mida de la memòria cau + cacheSizeTotal: Mida total acumulada de la memòria cau + files: Diferència en el nombre de fitxers + filesTotal: Nombre acumulat de fitxers + notesTotal: Nombre acumulat de notes + ff: "Diferència en el nombre d'usuaris seguits/seguidors seguits " +_timelines: + home: Inici + local: Local + recommended: Recomanat + social: Social + global: Global +_menuDisplay: + hide: Amagar + top: Superior + sideFull: Costat + sideIcon: Costat (Icones) +_wordMute: + muteWords: Paraules silenciades + muteWordsDescription: Separeu amb espais per a una condició AND o amb salts de línia + per a una condició OR. + soft: Suau + hard: Dur + muteWordsDescription2: Envolta les paraules clau amb barres inclinades per utilitzar + expressions regulars. + softDescription: Amaga les publicacions que compleixen les condicions establertes + de la línia de temps. + hardDescription: Evita que les publicacions que compleixin les condicions establertes + s'afegeixin a la línia de temps. A més, aquestes publicacions no s'afegiran a + la línia de temps encara que es modifiquin les condicions. + mutedNotes: Notes silenciades +_auth: + shareAccessAsk: Estàs segur que vols autoritzar aquesta aplicació per accedir al + teu compte? + shareAccess: Vols autoritzar "{name}" per accedir a aquest compte? + permissionAsk: Aquesta aplicació sol·licita els següents permisos + callback: Tornant a l'aplicació + denied: Accés denegat + pleaseGoBack: Si us plau, torneu a l'aplicació + copyAsk: Enganxeu el següent codi d'autorització a l'aplicació +_weekday: + wednesday: Dimecres + saturday: Dissabte + monday: Dilluns + tuesday: Dimarts + friday: Divendres + sunday: Diumenge + thursday: Dijous +_messaging: + groups: Grups + dms: Privat +_antennaSources: + all: Totes les notes + homeTimeline: Notes dels usuaris que segueixes + users: Notes d'usuaris concrets + userGroup: Notes d'usuaris d'un grup determinat + userList: Notes d'una llista determinada d'usuaris + instances: Notes de tots els usuaris d'una instància +_relayStatus: + requesting: Pendent + accepted: Acceptat + rejected: Rebutjat +_apps: + crossPlatform: Multiplataforma + mobile: Mòbil + firstParty: Primer partit + secondClass: Segona classe + thirdClass: Tercera classe + pwa: Instal·lar PWA + kaiteki: Kaiteki + milktea: Milktea + missLi: MissLi + mona: Mona + lesskey: Lesskey + firstClass: Primera classe + free: Gratuït + paid: Pagament + theDesk: TheDesk + apps: Aplicacions diff --git a/locales/en-US.yml b/locales/en-US.yml index 2feb2cd947..4acb0fc5b1 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" diff --git a/locales/fi.yml b/locales/fi.yml index c9e5e06e08..9b4102ddcb 100644 --- a/locales/fi.yml +++ b/locales/fi.yml @@ -1,9 +1,10 @@ +_lang_: "Suomi" username: Käyttäjänimi fetchingAsApObject: Hae Fedeversestä gotIt: Selvä! cancel: Peruuta enterUsername: Anna käyttäjänimi -renotedBy: Buustannut {käyttäjä} +renotedBy: Buustannut {user} noNotes: Ei lähetyksiä noNotifications: Ei ilmoituksia instance: Instanssi @@ -41,3 +42,182 @@ 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. diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 466212ba21..8ae43cdb9b 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/zh-CN.yml b/locales/zh-CN.yml index c652b52b7d..645f11f568 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 eb640b7dd4..c2dfd1ce02 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1012,9 +1012,9 @@ _mfm: blockCode: "程式碼(區塊)" blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。" inlineMath: "數學公式(內嵌)" - inlineMathDescription: "顯示內嵌的KaTex數學公式。" + inlineMathDescription: "顯示內嵌的KaTeX數學公式。" blockMath: "數學公式(方塊)" - blockMathDescription: "以區塊顯示複數行的KaTex數學式。" + blockMathDescription: "以區塊顯示KaTeX數學式。" quote: "引用" quoteDescription: "可以用來表示引用的内容。" emoji: "自訂表情符號" diff --git a/package.json b/package.json index 454ba4d30e..c4fc9a2996 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "calckey", - "version": "13.2.0-beta9h", + "version": "14.0.0-rc", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico index 96effef17d..11a614ae72 100644 Binary files a/packages/backend/assets/favicon.ico and b/packages/backend/assets/favicon.ico differ diff --git a/packages/backend/migration/1682777547198-LibreTranslate.js b/packages/backend/migration/1682777547198-LibreTranslate.js new file mode 100644 index 0000000000..dbaf483e6c --- /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 0000000000..babe64883a --- /dev/null +++ b/packages/backend/migration/1682891890317-InstanceSilence.js @@ -0,0 +1,13 @@ +export class InstanceSilence1682891890317 { + name = "InstanceSilence1682891890317"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`); + } +} diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 4f367debe0..0cd8c02adc 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 6e46232428..35ed307931 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 26a7c9c193..84f9af4793 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 fb4498911a..667ec948de 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 ed3369bf11..f793d40f62 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 29ac726efc..042ab446c7 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -10,7 +10,13 @@ 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,22 +336,68 @@ 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; + return; + } + // This may be used before the follow is completed, so we do not + // check if the following exists. + + const [follower, followee] = await Promise.all([ + Users.findOneBy({ + id: ctx.params.follower, + host: IsNull(), + }), + Users.findOneBy({ + id: ctx.params.followee, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + ctx.status = 404; + return; + } + + ctx.body = renderActivity(renderFollow(follower, followee)); + 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"); + } + 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; } - // This may be used before the follow is completed, so we do not - // check if the following exists. + + 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: ctx.params.follower, + id: followRequest.followerId, host: IsNull(), }), Users.findOneBy({ - id: ctx.params.followee, + id: followRequest.followeeId, host: Not(IsNull()), }), ]); @@ -355,13 +407,13 @@ router.get("/follows/:follower/:followee", async (ctx) => { return; } - ctx.body = renderActivity(renderFollow(follower, followee)); 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); }); 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 15ad1f9a17..a7b6e95c28 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 c8c639f504..89928af11c 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 f7e79b64b5..7f92e5e29e 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 8f6184b196..646f38282b 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 4dc1c941e3..23989750fc 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 c6415ceef2..d86fc12a2e 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 06b3ea4efa..3568a27b25 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 e8dfe52812..df85f21624 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,9 @@ 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 +87,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 +106,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 0000000000..825d0f5183 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -0,0 +1,61 @@ +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 7490581937..deb5dac309 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -3,8 +3,14 @@ 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 +68,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 +85,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); @@ -115,11 +117,9 @@ 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; @@ -136,9 +136,7 @@ export function apiAccountMastodon(router: Router): void { 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; + ctx.body = convertAccount(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -155,27 +153,9 @@ export function apiAccountMastodon(router: Router): void { try { const data = await client.getAccountStatuses( convertId(ctx.params.id, IdType.CalckeyId), - argsToBools(limitToInt(ctx.query as any)), + convertTimelinesArgsId(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; + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -193,13 +173,9 @@ export function apiAccountMastodon(router: Router): void { try { const data = await client.getAccountFollowers( convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), + convertTimelinesArgsId(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; + ctx.body = data.data.map((account) => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -217,13 +193,9 @@ export function apiAccountMastodon(router: Router): void { try { const data = await client.getAccountFollowing( convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), + convertTimelinesArgsId(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; + ctx.body = data.data.map((account) => convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -239,8 +211,10 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getAccountLists(ctx.params.id); - ctx.body = data.data; + 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); @@ -259,9 +233,8 @@ export function apiAccountMastodon(router: Router): void { const data = await client.followAccount( convertId(ctx.params.id, IdType.CalckeyId), ); - let acct = data.data; + let acct = convertRelationship(data.data); acct.following = true; - acct.id = convertId(acct.id, IdType.MastodonId); ctx.body = acct; } catch (e: any) { console.error(e); @@ -281,8 +254,7 @@ export function apiAccountMastodon(router: Router): void { const data = await client.unfollowAccount( convertId(ctx.params.id, IdType.CalckeyId), ); - let acct = data.data; - acct.id = convertId(acct.id, IdType.MastodonId); + let acct = convertRelationship(data.data); acct.following = false; ctx.body = acct; } catch (e: any) { @@ -303,9 +275,7 @@ export function apiAccountMastodon(router: Router): void { 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; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -324,9 +294,7 @@ export function apiAccountMastodon(router: Router): void { 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; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -346,9 +314,7 @@ export function apiAccountMastodon(router: Router): void { 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; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -367,9 +333,7 @@ export function apiAccountMastodon(router: Router): void { 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; + ctx.body = convertRelationship(data.data); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -383,28 +347,10 @@ export function apiAccountMastodon(router: Router): void { 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; + const data = await client.getBookmarks( + 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 +363,10 @@ 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 +379,10 @@ 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 +395,10 @@ 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 +414,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 +432,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 +451,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 d21bc1d330..c99031b0c7 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 8508f1d486..ac091855f4 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,12 @@ 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 +48,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 +86,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 e4990811ae..e1aec3488b 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,20 @@ 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 +60,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 +72,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 f7589569c9..a479140e08 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -4,7 +4,14 @@ 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 +25,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 +63,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 +75,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 +90,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 +113,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 +139,12 @@ 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 +160,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 +186,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 +207,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 +227,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 +246,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 +265,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 +284,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 +303,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 +322,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 +338,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 +354,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 +369,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 +387,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 57e5d9bb02..b8ef0929ee 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,14 @@ 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 +133,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 +150,11 @@ 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 +170,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 +188,9 @@ 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 +205,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 +220,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 +238,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 +253,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 +273,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 +293,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 +313,10 @@ 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 +335,10 @@ 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 d24a74c12a..440d0e3686 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 642a17d578..f1e0ed6920 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -399,28 +399,33 @@ 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); - ctx.set("Cache-Control", "public, max-age=15"); + 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, + }); - return; - } + ctx.set("Cache-Control", "public, max-age=15"); + + return; + } + } catch {} await next(); }); diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index f6545b131c..e2dd3fc332 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 61a8c6b268..3a77676b38 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 8b2e86ab5b..50dbd9b3be 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 5dd324d89a..f1164c9c6e 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 1a3c52eb51..277393eb41 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 f22128f047..0000000000 --- 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 164cf1fbe8..0000000000 --- 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 d68f241752..598dd1cdbc 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 bef00da4ea..478b86721c 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 49c175b15b..1735855037 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/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue index 01ab113516..107e140e73 100644 --- a/packages/client/src/components/MkCwButton.vue +++ b/packages/client/src/components/MkCwButton.vue @@ -28,7 +28,7 @@ const emit = defineEmits<{ (ev: "update:modelValue", v: boolean): void; }>(); -const el = ref(); +const el = ref(); const label = computed(() => { return concat([ @@ -52,7 +52,7 @@ function focus() { } defineExpose({ - focus + focus, }); @@ -73,9 +73,46 @@ defineExpose({ } } } - &:hover > span, &:focus > span { + &:hover > span, + &:focus > span { background: var(--cwFg) !important; color: var(--cwBg) !important; } + + &.fade { + display: block; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + > span { + display: inline-block; + background: var(--panel); + padding: 0.4em 1em; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); + } + &:hover { + > span { + background: var(--panelHighlight); + } + } + } + &.showLess { + width: 100%; + margin-top: 1em; + position: sticky; + bottom: var(--stickyBottom); + + > span { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 0 7px 7px var(--bg); + } + } } diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index 88d207babb..3dd54ed840 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -174,7 +174,7 @@ import { deviceKind } from "@/scripts/device-kind"; import { emojiCategories, instance } from "@/instance"; import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; -import { FocusTrap } from 'focus-trap-vue'; +import { FocusTrap } from "focus-trap-vue"; const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue index 0a3fbbea2f..6bc46c0e4c 100644 --- a/packages/client/src/components/MkInstanceCardMini.vue +++ b/packages/client/src/components/MkInstanceCardMini.vue @@ -5,6 +5,7 @@ { yellow: instance.isNotResponding, red: instance.isBlocked, + purple: instance.isSilenced, gray: instance.isSuspended, }, ]" @@ -23,13 +24,13 @@ + diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index 94869402f8..c84f7f0641 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -35,10 +35,19 @@ class="content" :class="{ collapsed, isLong, showContent: note.cw && !showContent }" > - -
+
({{ i18n.ts.deleted }})
- - + -