Merge branch 'develop' into refactor/antennas-in-cache
This commit is contained in:
commit
04c43ed3ef
|
@ -0,0 +1,13 @@
|
|||
Copyright 2023 Calckey
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -121,7 +121,7 @@ redis:
|
|||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Maximum length of a post (default 3000, max 8192)
|
||||
# Maximum length of a post (default 3000, max 100000)
|
||||
#maxNoteLength: 3000
|
||||
|
||||
# Maximum length of an image caption (default 1500, max 8192)
|
||||
|
|
|
@ -25,6 +25,7 @@ coverage
|
|||
!/.config/devenv.yml
|
||||
!/.config/docker_example.env
|
||||
!/.config/helm_values_example.yml
|
||||
!/.config/LICENSE
|
||||
|
||||
#docker dev config
|
||||
/dev/docker-compose.yml
|
||||
|
|
10
CALCKEY.md
10
CALCKEY.md
|
@ -6,19 +6,13 @@
|
|||
## Planned
|
||||
|
||||
- Stucture
|
||||
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
||||
- Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes
|
||||
- Rewrite backend in Rust and [Rocket](https://rocket.rs/)
|
||||
- Use [Magic RegExP](https://regexp.dev/) for RegEx 🦄
|
||||
- Function
|
||||
- User "choices" (recommended users) and featured hashtags like Mastodon and Soapbox
|
||||
- Join Reason system like Mastodon/Pleroma
|
||||
- Option to publicize server blocks
|
||||
- More antenna options
|
||||
- Groups
|
||||
- Form
|
||||
- Lookup/details for post/file/server
|
||||
- [Rat mode?](https://stop.voring.me/notes/933fx97bmd)
|
||||
|
||||
## Work in progress
|
||||
|
||||
|
@ -30,6 +24,7 @@
|
|||
- Timeline filters
|
||||
- Events
|
||||
- Fully revamp non-logged-in screen
|
||||
- Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes
|
||||
|
||||
## Implemented
|
||||
|
||||
|
@ -122,6 +117,7 @@
|
|||
- Let moderators see moderation nodes
|
||||
- Non-mangled unicode emojis
|
||||
- Skin tone selection support
|
||||
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
||||
|
||||
## Implemented (remote)
|
||||
|
||||
|
@ -137,7 +133,7 @@
|
|||
- 👍 also triggers generic like/favorite
|
||||
- [Add additional background for acrylic popups if backdrop-filter is unsupported](https://github.com/misskey-dev/misskey/pull/8671)
|
||||
- [Add parameters to MFM rotate](https://github.com/misskey-dev/misskey/pull/8549)
|
||||
- Many changes from [Foundkey](https://akkoma.dev/FoundKeyGang/Foundkey)
|
||||
- Many changes from [FoundKey](https://akkoma.dev/FoundKeyGang/FoundKey)
|
||||
- https://akkoma.dev/FoundKeyGang/FoundKey/commit/0ece67b04c3f0365057624c1068808276ccab981: refactor pages/auth.form.vue to composition API
|
||||
- https://akkoma.dev/FoundKeyGang/FoundKey/commit/4bc9610d8bf5af736b5e89e4782395705de45d7d: remove unnecessary joins
|
||||
- https://akkoma.dev/FoundKeyGang/FoundKey/commit/9ee609d70082f7a6dc119a5d83c0e7c5e1208676: enhance privacy of notes
|
||||
|
|
|
@ -2704,7 +2704,7 @@ Co-committed-by: naskya <naskya@noreply.codeberg.org>
|
|||
|
||||
Passwords will be automatically re-hashed on sign-in. All new password hashes will be argon2 by default. This uses argon2id and is not configurable. In the very unlikely case someone has more specific needs, a fork is recommended. ChangeLog: Added Co-authored-by: Chloe Kudryavtsev <code@toast.bunkerlabs.net>
|
||||
|
||||
Breaks Calckey -> Misskey migration, but fixes Foundkey -> Calckey migration
|
||||
Breaks Calckey -> Misskey migration, but fixes FoundKey -> Calckey migration
|
||||
|
||||
- Add argon
|
||||
|
||||
|
|
27
COPYING
27
COPYING
|
@ -1,15 +1,24 @@
|
|||
Unless otherwise stated this repository is
|
||||
Copyright © 2014-2022 syuilo and contributers
|
||||
Copyright © 2022 thatonecalculator and contributers
|
||||
Unless specified otherwise, the entirety of this repository is subject to the following:
|
||||
Copyright © 2014-2023 syuilo and contributors
|
||||
Copyright © 2022-2023 Kainoa Kanter and contributors
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
---
|
||||
|
||||
Calckey includes several third-party Open-Source softwares.
|
||||
These specific configuration directories:
|
||||
|
||||
Emoji keywords for Unicode 11 and below by Mu-An Chiou
|
||||
License: MIT
|
||||
https://github.com/muan/emojilib/blob/master/LICENSE
|
||||
- .config/
|
||||
- custom/assets/
|
||||
|
||||
and their contents are
|
||||
Copyright © 2022-2023 Kainoa Kanter and contributors
|
||||
|
||||
And are distributed under The Apache License, Version 2.0, you should have received a copy of the license file as LICENSE in each specified directory.
|
||||
|
||||
---
|
||||
|
||||
Calckey includes several third-party open-source softwares and software libraries.
|
||||
|
||||
RsaSignature2017 implementation by Transmute Industries Inc
|
||||
License: MIT
|
||||
|
@ -18,3 +27,7 @@ https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
|
|||
Machine learning model for sensitive images by Infinite Red, Inc.
|
||||
License: MIT
|
||||
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
|
||||
|
||||
Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository:
|
||||
|
||||
pnpm licenses list
|
||||
|
|
12
README.md
12
README.md
|
@ -72,6 +72,14 @@
|
|||
|
||||
# 🌠 Getting started
|
||||
|
||||
Want to just join a Calckey server? View the list here, pick one, and join:
|
||||
|
||||
### https://calckey.org/join
|
||||
|
||||
---
|
||||
|
||||
Want to make your own? Keep reading!
|
||||
|
||||
This guide will work for both **starting from scratch** and **migrating from Misskey**.
|
||||
|
||||
## 🔰 Easy installers
|
||||
|
@ -208,9 +216,9 @@ Please don't use ElasticSearch unless you already have an ElasticSearch setup an
|
|||
- Edit `.config/default.yml`, making sure to fill out required fields.
|
||||
- Also copy and edit `.config/docker_example.env` to `.config/docker.env` if you're using Docker.
|
||||
|
||||
## 🚚 Migrating from Misskey to Calckey
|
||||
## 🚚 Migrating from Misskey/FoundKey to Calckey
|
||||
|
||||
For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md).
|
||||
For migrating from Misskey v13, Misskey v12, and FoundKey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md).
|
||||
|
||||
## 🌐 Web proxy
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2023 Calckey
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -1,10 +1,11 @@
|
|||
# 🚚 Migrating from Misskey to Calckey
|
||||
# 🚚 Migrating from Misskey/FoundKey to Calckey
|
||||
|
||||
The following procedure may not work depending on your environment and version of Misskey.
|
||||
All the guides below assume you're starting in the root of the repo directory.
|
||||
|
||||
**Make sure you**
|
||||
- **stopped all master and worker processes of Misskey.**
|
||||
- **have backups of the database before performing any commands.**
|
||||
### Before proceeding
|
||||
|
||||
- **Ensure you have stopped all master and worker processes of Misskey.**
|
||||
- **Ensure you have backups of the database before performing any commands.**
|
||||
|
||||
## Misskey v13 and above
|
||||
|
||||
|
@ -77,15 +78,16 @@ NODE_ENV=production pnpm run migrate
|
|||
# build using prefered method
|
||||
```
|
||||
|
||||
## Foundkey
|
||||
## FoundKey
|
||||
|
||||
```sh
|
||||
cd packages/backend
|
||||
sed -i '12s/^/\/\//' ./migration/1663399074403-resize-comments-drive-file.js
|
||||
|
||||
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n uniformThemecolor1652859567549 | cut -d ':' -f 1)"
|
||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | wc -l)"
|
||||
|
||||
for i in $(seq 1 $NUM_MIGRAIONS); do
|
||||
for i in $(seq 1 $NUM_MIGRATIONS); do
|
||||
npx typeorm migration:revert -d ormconfig.js
|
||||
done
|
||||
|
||||
|
@ -100,4 +102,4 @@ NODE_ENV=production pnpm run migrate
|
|||
|
||||
## Reverse
|
||||
|
||||
You ***cannot*** migrate back to Misskey from Calckey due to re-hashing passwords on signin with argon2. You can migrate from Calckey to Foundkey, though.
|
||||
You ***cannot*** migrate back to Misskey from Calckey due to re-hashing passwords on signin with argon2. You can migrate from Calckey to FoundKey, although this is not recommended due to FoundKey being end-of-life.
|
||||
|
|
|
@ -95,7 +95,7 @@ privacy: "Privadesa"
|
|||
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
|
||||
defaultNoteVisibility: "Visibilitat per defecte"
|
||||
follow: "Segueix"
|
||||
followRequest: "Segueix"
|
||||
followRequest: "Sol·licitud de Seguiment"
|
||||
followRequests: "Sol·licituds de seguiment"
|
||||
unfollow: "Deixa de seguir"
|
||||
followRequestPending: "Sol·licituds de seguiment pendents"
|
||||
|
@ -1382,7 +1382,7 @@ adminCustomCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què
|
|||
showUpdates: Mostra una finestra emergent quan Calckey s'actualitzi
|
||||
recommendedInstances: Servidors recomanats
|
||||
recommendedInstancesDescription: Servidors recomanats separats per salts de línia
|
||||
que apareixen a la línia de temps recomanada. NO afegiu `https://`, NOMÉS el domini.
|
||||
que apareixen a la línia de temps recomanada.
|
||||
caption: Descripció Automàtica
|
||||
splash: Pantalla de Benvinguda
|
||||
swipeOnDesktop: Permet lliscar a l'estil del mòbil a l'escriptori
|
||||
|
@ -1603,6 +1603,13 @@ _aboutMisskey:
|
|||
patrons: Mecenes de Calckey
|
||||
patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació
|
||||
amb l'enllaç de dalt per veure el teu nom aquí!
|
||||
donateTitle: T'agrada Calckey?
|
||||
pleaseDonateToCalckey: Penseu en fer una donació a Calckey per donar suport al seu
|
||||
desenvolupament.
|
||||
pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host},
|
||||
per ajudar-lo a suportar els costos de funcionament.
|
||||
donateHost: Fes una donació a {host}
|
||||
sponsors: Patrocinadors de Calckey
|
||||
unknown: Desconegut
|
||||
pageLikesCount: Nombre de pàgines amb M'agrada
|
||||
youAreRunningUpToDateClient: Estás fent servir la versió del client més nova.
|
||||
|
@ -2144,3 +2151,13 @@ _skinTones:
|
|||
swipeOnMobile: Permet lliscar entre pàgines
|
||||
enableIdenticonGeneration: Habilitar la generació d'Identicon
|
||||
enableServerMachineStats: Habilitar les estadístiques del maquinari del servidor
|
||||
showPopup: Notificar els usuaris amb una finestra emergent
|
||||
showWithSparkles: Mostra amb espurnes
|
||||
youHaveUnreadAnnouncements: Tens anuncis sense llegir
|
||||
xl: XL
|
||||
donationLink: Enllaç a la pàgina de donacions
|
||||
neverShow: No tornis a mostrar
|
||||
remindMeLater: Potser després
|
||||
removeMember: Elimina el membre
|
||||
removeQuote: Elimina la cita
|
||||
removeRecipient: Elimina el destinatari
|
||||
|
|
|
@ -105,7 +105,7 @@ privacy: "Privacy"
|
|||
makeFollowManuallyApprove: "Follow requests require approval"
|
||||
defaultNoteVisibility: "Default visibility"
|
||||
follow: "Follow"
|
||||
followRequest: "Follow"
|
||||
followRequest: "Follow Request"
|
||||
followRequests: "Follow requests"
|
||||
unfollow: "Unfollow"
|
||||
followRequestPending: "Follow request pending"
|
||||
|
@ -644,6 +644,7 @@ useBlurEffectForModal: "Use blur effect for modals"
|
|||
useFullReactionPicker: "Use full-size reaction picker"
|
||||
width: "Width"
|
||||
height: "Height"
|
||||
xl: "XL"
|
||||
large: "Big"
|
||||
medium: "Medium"
|
||||
small: "Small"
|
||||
|
@ -1049,7 +1050,7 @@ customSplashIconsDescription: "URLs for custom splash screen icons separated by
|
|||
showUpdates: "Show a popup when Calckey updates"
|
||||
recommendedInstances: "Recommended servers"
|
||||
recommendedInstancesDescription: "Recommended servers separated by line breaks to
|
||||
appear in the recommended timeline. Do NOT add `https://`, ONLY the domain."
|
||||
appear in the recommended timeline."
|
||||
caption: "Auto Caption"
|
||||
splash: "Splash Screen"
|
||||
updateAvailable: "There might be an update available!"
|
||||
|
@ -1117,6 +1118,12 @@ enableIdenticonGeneration: "Enable Identicon generation"
|
|||
showPopup: "Notify users with popup"
|
||||
showWithSparkles: "Show with sparkles"
|
||||
youHaveUnreadAnnouncements: "You have unread announcements"
|
||||
donationLink: "Link to donation page"
|
||||
neverShow: "Don't show again"
|
||||
remindMeLater: "Maybe later"
|
||||
removeQuote: "Remove quote"
|
||||
removeRecipient: "Remove recipient"
|
||||
removeMember: "Remove member"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
@ -1215,8 +1222,13 @@ _aboutMisskey:
|
|||
source: "Source code"
|
||||
translation: "Translate Calckey"
|
||||
donate: "Donate to Calckey"
|
||||
donateTitle: "Enjoying Calckey?"
|
||||
pleaseDonateToCalckey: "Please consider donating to Calckey to support its development."
|
||||
pleaseDonateToHost: "Please also consider donating to your home server, {host}, to help support its operation costs."
|
||||
donateHost: "Donate to {host}"
|
||||
morePatrons: "We also appreciate the support of many other helpers not listed here.
|
||||
Thank you! 🥰"
|
||||
sponsors: "Calckey sponsors"
|
||||
patrons: "Calckey patrons"
|
||||
patronsList: "Listed chronologically, not by donation size. Donate with the link above to get your name on here!"
|
||||
_nsfw:
|
||||
|
|
|
@ -642,7 +642,7 @@ wordMute: "Silenciar palabras"
|
|||
regexpError: "Error de la expresión regular"
|
||||
regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line}
|
||||
de las palabras muteadas {tab}"
|
||||
instanceMute: "Instancias silenciadas"
|
||||
instanceMute: "Servidores silenciados"
|
||||
userSaysSomething: "{name} dijo algo"
|
||||
makeActive: "Activar"
|
||||
display: "Apariencia"
|
||||
|
@ -671,14 +671,14 @@ sample: "Muestra"
|
|||
abuseReports: "Reportes"
|
||||
reportAbuse: "Reportar"
|
||||
reportAbuseOf: "Reportar a {name}"
|
||||
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en
|
||||
particular, ingrese la URL de esta."
|
||||
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una publicación
|
||||
en particular, ingrese la URL de esta."
|
||||
abuseReported: "Se ha enviado el reporte. Muchas gracias."
|
||||
reporter: "Reportador"
|
||||
reporteeOrigin: "Reportar a"
|
||||
reporterOrigin: "Origen del reporte"
|
||||
forwardReport: "Transferir un informe a una instancia remota"
|
||||
forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá
|
||||
forwardReport: "Transferir reporte a un servidor remoto"
|
||||
forwardReportIsAnonymous: "No puede ver su información del servidor remoto y aparecerá
|
||||
como una cuenta anónima del sistema"
|
||||
send: "Enviar"
|
||||
abuseMarkAsResolved: "Marcar reporte como resuelto"
|
||||
|
@ -686,7 +686,7 @@ openInNewTab: "Abrir en una Nueva Pestaña"
|
|||
openInSideView: "Abrir en una vista al costado"
|
||||
defaultNavigationBehaviour: "Navegación por defecto"
|
||||
editTheseSettingsMayBreakAccount: "Editar estas configuraciones puede dañar su cuenta."
|
||||
instanceTicker: "Información de notas de la instancia"
|
||||
instanceTicker: "Información de publicaciones de el servidor"
|
||||
waitingFor: "Esperando a {x}"
|
||||
random: "Aleatorio"
|
||||
system: "Sistema"
|
||||
|
@ -697,14 +697,14 @@ createNew: "Crear"
|
|||
optional: "Opcional"
|
||||
createNewClip: "Crear clip nuevo"
|
||||
unclip: "Quitar clip"
|
||||
confirmToUnclipAlreadyClippedNote: "Esta nota ya está incluida en el clip \"{name}\"\
|
||||
. ¿Quiere quitar la nota del clip?"
|
||||
confirmToUnclipAlreadyClippedNote: "Esta publicación ya está incluida en el clip \"\
|
||||
{name}\". ¿Quiere quitar la nota del clip?"
|
||||
public: "Público"
|
||||
i18nInfo: "Calckey está siendo traducido a varios idiomas gracias a voluntarios. Se
|
||||
puede colaborar traduciendo en {link}"
|
||||
manageAccessTokens: "Administrar tokens de acceso"
|
||||
accountInfo: "Información de la Cuenta"
|
||||
notesCount: "Cantidad de notas"
|
||||
notesCount: "Cantidad de publicaciones"
|
||||
repliesCount: "Cantidad de respuestas hechas"
|
||||
renotesCount: "Cantidad de renotas hechas"
|
||||
repliedCount: "Cantidad de respuestas recibidas"
|
||||
|
@ -720,7 +720,7 @@ no: "No"
|
|||
driveFilesCount: "Cantidad de archivos en el drive"
|
||||
driveUsage: "Uso del drive"
|
||||
noCrawle: "Rechazar indexación del crawler"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas,
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, publicaciones,
|
||||
páginas, etc."
|
||||
lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"Sólo
|
||||
seguidores\", tus notas serán visibles para cualquiera, incluso si requieres que
|
||||
|
@ -734,7 +734,7 @@ verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación
|
|||
configuración."
|
||||
notSet: "Sin especificar"
|
||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||
noteFavoritesCount: "Número de notas favoritas"
|
||||
noteFavoritesCount: "Número de publicaciones favoritas"
|
||||
pageLikesCount: "Número de favoritos en la página"
|
||||
pageLikedCount: "Número de favoritos de su página"
|
||||
contact: "Contacto"
|
||||
|
@ -975,7 +975,7 @@ shuffle: "Aleatorio"
|
|||
account: "Cuentas"
|
||||
move: "Mover"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento
|
||||
description: "Reduce el esfuerzo de la moderación de el servidor a través del reconocimiento
|
||||
automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar
|
||||
ligeramente la carga en el servidor."
|
||||
sensitivity: "Sensibilidad de detección"
|
||||
|
@ -1295,7 +1295,7 @@ _time:
|
|||
_tutorial:
|
||||
title: "Cómo usar Calckey"
|
||||
step1_1: "¡Bienvenido!"
|
||||
step1_2: "Vamos a configurarte. Estarás listo y funcionando en poco tiempo"
|
||||
step1_2: "Vamos a configurarte. ¡Estarás listo y funcionando en poco tiempo!"
|
||||
step2_1: "En primer lugar, rellena tu perfil"
|
||||
step2_2: "Proporcionar algo de información sobre quién eres hará que sea más fácil
|
||||
para los demás saber si quieren ver tus notas o seguirte."
|
||||
|
@ -1789,7 +1789,7 @@ _pages:
|
|||
splitStrByLine: "Separar texto en lineas"
|
||||
_splitStrByLine:
|
||||
arg1: "Texto"
|
||||
ref: "Variables"
|
||||
ref: "Variable"
|
||||
aiScriptVar: "Variable de AiScript"
|
||||
fn: "funciones"
|
||||
_fn:
|
||||
|
@ -1800,8 +1800,8 @@ _pages:
|
|||
_for:
|
||||
arg1: "Cantidad de repeticiones"
|
||||
arg2: "Acción"
|
||||
typeError: "El slot {slot} acepta el tipo {expect} pero fue ingresado el tipo
|
||||
{actual}"
|
||||
typeError: "El slot {slot} acepta el tipo \"{expect}\" pero fue ingresado el tipo
|
||||
\"{actual}\""
|
||||
thereIsEmptySlot: "El slot {slot} está vacío"
|
||||
types:
|
||||
string: "Texto"
|
||||
|
|
|
@ -1952,8 +1952,7 @@ antennaInstancesDescription: Lister un hôte d'instance par ligne
|
|||
userSaysSomethingReason: '{name} a dit {reason}'
|
||||
breakFollowConfirm: Êtes vous sur de vouloir retirer l'abonné ?
|
||||
recommendedInstancesDescription: Instances recommandées séparées par une nouvelle
|
||||
ligne pour apparaître dans la timeline recommandée. Ne PAS ajouter `https://`, SEULEMENT
|
||||
le domaine.
|
||||
ligne pour apparaître dans la timeline recommandée.
|
||||
sendPushNotificationReadMessage: Supprimer les notifications push une fois que les
|
||||
notifications ou messages concernés ont été lus
|
||||
sendPushNotificationReadMessageCaption: Une notification contenant le texte "{emptyPushNotificationMessage}"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
_lang_: Inglés
|
||||
introMisskey: Benvida! Calckey é unha plataforma de medios sociais de código aberto,
|
||||
descentralizada e gratuíta para sempre!🚀
|
||||
monthAndDay: '{day}/{month}'
|
||||
notifications: Notificacións
|
||||
password: Contrasinal
|
||||
forgotPassword: Esquecín o contrasinal
|
||||
gotIt: Vale!
|
||||
cancel: Cancelar
|
||||
noThankYou: Non, grazas
|
||||
headlineMisskey: Plataforma de medios sociais de código aberto e descentralizada,
|
||||
gratuíta para sempre!🚀
|
||||
search: Buscar
|
||||
searchPlaceholder: Buscar en Calckey
|
||||
username: Identificador
|
||||
fetchingAsApObject: Descargando desde o Fediverso
|
||||
ok: OK
|
|
@ -946,7 +946,7 @@ customSplashIconsDescription: "ユーザがページをロード/リロードす
|
|||
URL。画像は静的なURLで、できればすべて192x192にリサイズしてください。"
|
||||
showUpdates: "Calckeyの更新時にポップアップを表示する"
|
||||
recommendedInstances: "おすすめサーバー"
|
||||
recommendedInstancesDescription: "おすすめタイムラインに表示するサーバーを改行区切りで入力してください。`https://`は書かず、ドメインのみを入力してください。"
|
||||
recommendedInstancesDescription: "おすすめタイムラインに表示するサーバーを改行区切りで入力してください。"
|
||||
caption: "自動キャプション"
|
||||
splash: "スプラッシュスクリーン"
|
||||
updateAvailable: "アップデートがありますよ!"
|
||||
|
@ -983,6 +983,8 @@ enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にす
|
|||
showPopup: "ポップアップを表示してユーザーに知らせる"
|
||||
showWithSparkles: "タイトルをキラキラさせる"
|
||||
youHaveUnreadAnnouncements: "未読のお知らせがあります"
|
||||
neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||
|
@ -1068,6 +1070,10 @@ _aboutMisskey:
|
|||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
|
||||
patrons: "支援者"
|
||||
patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう!
|
||||
pleaseDonateToCalckey: Calckey開発への寄付をご検討ください。
|
||||
pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。
|
||||
donateHost: '{host} に寄付する'
|
||||
donateTitle: Calckeyを気に入りましたか?
|
||||
_nsfw:
|
||||
respect: "閲覧注意のメディアは隠す"
|
||||
ignore: "閲覧注意のメディアを隠さない"
|
||||
|
@ -1948,3 +1954,8 @@ removeReaction: リアクションを取り消す
|
|||
alt: 代替テキスト
|
||||
swipeOnMobile: ページ間のスワイプを有効にする
|
||||
reactionPickerSkinTone: 優先する絵文字のスキン色
|
||||
xl: 特大
|
||||
donationLink: 寄付ページへのリンク
|
||||
removeMember: メンバーを削除
|
||||
removeQuote: 引用を削除
|
||||
removeRecipient: 宛先を削除
|
||||
|
|
|
@ -1,2 +1,83 @@
|
|||
---
|
||||
_lang_: "Norsk Bokmål"
|
||||
search: Søk
|
||||
monthAndDay: '{day}/{month}'
|
||||
fetchingAsApObject: Henter fra fediverset
|
||||
ok: OK
|
||||
gotIt: Jeg forstår!
|
||||
profile: Profil
|
||||
timeline: Tidslinje
|
||||
save: Lagre
|
||||
addToList: Legg til liste
|
||||
searchPlaceholder: Søk Calckey
|
||||
username: Brukernavn
|
||||
password: Passord
|
||||
notifications: Meldinger
|
||||
forgotPassword: Glemt passord
|
||||
cancel: Avbryt
|
||||
noNotes: Ingen poster
|
||||
instance: Server
|
||||
settings: Innstillinger
|
||||
noAccountDescription: Denne brukeren har ikke fylt ut bio'en sin ennå.
|
||||
login: Logg inn
|
||||
loggingIn: Logger inn
|
||||
signup: Oppretter bruker
|
||||
uploading: Laster opp..
|
||||
enterUsername: Skriv inn brukernavn
|
||||
noNotifications: Ingen meldinger
|
||||
users: Brukere
|
||||
addUser: Legg til en bruker
|
||||
favorite: Legg til i bokmerker
|
||||
cantFavorite: Kunne ikke legges til i bokmerker.
|
||||
pin: Fest til profilen
|
||||
copyContent: Kopier innhold
|
||||
deleteAndEdit: Slett og rediger
|
||||
sendMessage: Send en melding
|
||||
copyUsername: Kopier brukernavn
|
||||
reply: Svar
|
||||
loadMore: Last mer
|
||||
showLess: Lukk
|
||||
receiveFollowRequest: Følgeforespørsel mottatt
|
||||
directNotes: Direktemelding
|
||||
importAndExport: Importer/eksporter data
|
||||
importRequested: Du har bedt om en importering. Dette vil ta litt tid.
|
||||
lists: Lister
|
||||
listsDesc: Lister lar deg lage tidslinjer med utvalgte brukere. De kan hentes frem
|
||||
fra tidslinje-siden.
|
||||
deleted: Slettet
|
||||
editNote: Rediger notat
|
||||
followsYou: Følger deg
|
||||
createList: Lag liste
|
||||
newer: nyere
|
||||
older: eldre
|
||||
download: Last ned
|
||||
unfollowConfirm: Er du sikker på at du ikke lenger vil følge {name}?
|
||||
noLists: Du har ingen lister
|
||||
following: Følger
|
||||
files: Filer
|
||||
note: Post
|
||||
notes: Poster
|
||||
followers: Følgere
|
||||
otherSettings: Andre innstillinger
|
||||
addInstance: Legg til en server
|
||||
alreadyFavorited: Allerede lagt til i bokmerker.
|
||||
delete: Slett
|
||||
openInWindow: Åpne i vindu
|
||||
basicSettings: Grunnleggende innstillinger
|
||||
headlineMisskey: En desentralisert sosialt media-plattform, basert på åpen kildekode,
|
||||
som alltid vil være gratis! 🚀
|
||||
introMisskey: Velkommen! Calckey er en desentralisert sosialt media-plattform, basert
|
||||
på åpen kildekode, som alltid vil være gratis! 🚀
|
||||
exportRequested: Du har bedt om en eksportering. Dette vil ta litt tid. Den vil bli
|
||||
lagt til på disken din når den er ferdig.
|
||||
noThankYou: Nei takk
|
||||
favorites: Bokmerker
|
||||
unfavorite: Fjern fra bokmerker
|
||||
favorited: Lagt til i bokmerker.
|
||||
copyLink: Kopier lenke
|
||||
searchUser: Søk etter en bruker
|
||||
jumpToPrevious: Gå til foregående
|
||||
showMore: Vis mer
|
||||
followRequestAccepted: Følgeforespørsel godtatt
|
||||
import: Importer
|
||||
export: Eksporter
|
||||
logout: Logger ut
|
||||
|
|
|
@ -85,3 +85,28 @@ noLists: Você não possui nenhuma lista
|
|||
following: Seguindo
|
||||
followers: Seguidores
|
||||
followsYou: Segue você
|
||||
fetchingAsApObject: Buscando do Fediverse
|
||||
timeline: Linha do tempo
|
||||
favorite: Adicionar aos marcadores
|
||||
favorites: Marcadores
|
||||
unfavorite: Remover dos marcadores
|
||||
favorited: Adicionado aos marcadores.
|
||||
alreadyFavorited: Já foi adicionado aos marcadores.
|
||||
download: Download
|
||||
pageLoadError: Ocorreu um erro ao carregar a página.
|
||||
pageLoadErrorDescription: Isso normalmente é causado por erros de rede ou pelo cache
|
||||
do navegador. Tente limpar o cache e, depois de esperar um pouquinho, tente novamente.
|
||||
serverIsDead: Esse servidos não está respondendo. Por favor espere um pouco e tente
|
||||
novamente.
|
||||
youShouldUpgradeClient: Para visualizar essa página, favor reiniciar para atualizar
|
||||
seu cliente.
|
||||
enterListName: Insira um nome para a lista
|
||||
privacy: Privacidade
|
||||
defaultNoteVisibility: Visibilidade padrão
|
||||
makeFollowManuallyApprove: Pedidos de seguimento precisam de aprovação
|
||||
follow: Seguir
|
||||
followRequest: Seguir
|
||||
followRequests: Pedidos de seguimento
|
||||
unfollow: Parar de seguir
|
||||
followRequestPending: Pedido de seguimento pendente
|
||||
enterEmoji: Insira um emoji
|
||||
|
|
|
@ -1847,7 +1847,7 @@ customMOTDDescription: Пользовательские сообщения дл
|
|||
разрывами строк, будут отображаться случайным образом каждый раз, когда пользователь
|
||||
загружает / перезагружает страницу.
|
||||
recommendedInstancesDescription: Рекомендуемые инстансы, разделенные разрывами строк,
|
||||
должны отображаться на рекомендуемой ленте. НЕ добавляйте `https://`, ТОЛЬКО домен.
|
||||
должны отображаться на рекомендуемой ленте.
|
||||
caption: Автоматическая подпись
|
||||
splash: Заставка
|
||||
updateAvailable: Возможно, доступно обновление!
|
||||
|
|
2063
locales/tr-TR.yml
2063
locales/tr-TR.yml
File diff suppressed because it is too large
Load Diff
1016
locales/uk-UA.yml
1016
locales/uk-UA.yml
File diff suppressed because it is too large
Load Diff
|
@ -1839,7 +1839,7 @@ pushNotification: 推送通知
|
|||
subscribePushNotification: 啟用推送通知
|
||||
unsubscribePushNotification: 禁用推送通知
|
||||
pushNotificationAlreadySubscribed: 推送通知已經啟用
|
||||
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。 不要添加 `https://`,只添加域名。
|
||||
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。
|
||||
searchPlaceholder: 在聯邦網路上搜尋
|
||||
cw: 內容警告
|
||||
selectChannel: 選擇一個頻道
|
||||
|
|
26
package.json
26
package.json
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "14.0.0-rc3",
|
||||
"version": "14.0.0-dev79",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.6",
|
||||
"packageManager": "pnpm@8.6.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
|
@ -21,13 +21,13 @@
|
|||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.js",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r run lint",
|
||||
"lint": "pnpm -r --parallel run lint",
|
||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "cypress run",
|
||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "pnpm -r run format",
|
||||
"format": "pnpm -r --parallel run format",
|
||||
"clean": "pnpm node ./scripts/clean.js",
|
||||
"clean-all": "pnpm node ./scripts/clean-all.js",
|
||||
"cleanall": "pnpm run clean-all"
|
||||
|
@ -36,17 +36,17 @@
|
|||
"chokidar": "^3.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.2.0",
|
||||
"@bull-board/ui": "5.2.0",
|
||||
"@bull-board/api": "5.6.0",
|
||||
"@bull-board/ui": "5.6.0",
|
||||
"@napi-rs/cli": "^2.16.1",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.18",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/node": "20.4.1",
|
||||
"chalk": "4.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
|
@ -57,8 +57,8 @@
|
|||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"rome": "^12.1.3",
|
||||
"rome": "^v12.1.3-nightly.f65b0d9",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
export class tweakVarcharLength1678426061773 {
|
||||
name = "tweakVarcharLength1678426061773";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ALTER COLUMN "smtpUser" TYPE character varying(1024)`,
|
||||
undefined,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ALTER COLUMN "smtpPass" TYPE character varying(1024)`,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export class DonationLink1689136347561 {
|
||||
name = "DonationLink1689136347561";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "donationLink" character varying(256)`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" DROP COLUMN "DonationLink1689136347561"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@
|
|||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"format": "cargo fmt --all",
|
||||
"lint": "cargo clippy --fix",
|
||||
"cargo:test": "pnpm run cargo:unit && pnpm run cargo:integration",
|
||||
"cargo:unit": "cargo test unit_test && cargo test -F napi unit_test",
|
||||
"cargo:integration": "cargo test -F noarray int_test -- --test-threads=1"
|
||||
|
|
|
@ -40,7 +40,7 @@ impl Repository<Antenna> for antenna::Model {
|
|||
src: self.src.try_into()?,
|
||||
user_list_id: self.user_list_id,
|
||||
user_group_id,
|
||||
users: self.users.into(),
|
||||
users: self.users,
|
||||
instances: self.instances.into(),
|
||||
case_sensitive: self.case_sensitive,
|
||||
notify: self.notify,
|
||||
|
|
|
@ -58,7 +58,7 @@ impl TryFrom<AntennaSrcEnum> for super::AntennaSrc {
|
|||
|
||||
// ---- TODO: could be macro
|
||||
impl Schema<Self> for super::Antenna {}
|
||||
pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(|| super::Antenna::validator());
|
||||
pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(super::Antenna::validator);
|
||||
// ----
|
||||
|
||||
cfg_if! {
|
||||
|
|
|
@ -91,7 +91,7 @@ pub enum AppPermission {
|
|||
|
||||
impl Schema<Self> for App {}
|
||||
|
||||
pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(|| App::validator());
|
||||
pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(App::validator);
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
|
|
|
@ -148,8 +148,8 @@ async fn setup_model(db: &DbConn) {
|
|||
let user_model = entity::user::Model {
|
||||
id: user_id.to_owned(),
|
||||
created_at: Utc::now().into(),
|
||||
username: name.to_lowercase().to_string(),
|
||||
username_lower: name.to_lowercase().to_string(),
|
||||
username: name.to_lowercase(),
|
||||
username_lower: name.to_lowercase(),
|
||||
name: Some(name.to_string()),
|
||||
token: Some(gen_string(16)),
|
||||
is_admin: true,
|
||||
|
|
|
@ -43,18 +43,16 @@ mod int_test {
|
|||
keywords: vec![
|
||||
vec!["foo".to_string(), "bar".to_string()],
|
||||
vec!["foobar".to_string()],
|
||||
]
|
||||
.into(),
|
||||
],
|
||||
exclude_keywords: vec![
|
||||
vec!["abc".to_string()],
|
||||
vec!["def".to_string(), "ghi".to_string()],
|
||||
]
|
||||
.into(),
|
||||
],
|
||||
src: schema::AntennaSrc::All,
|
||||
user_list_id: None,
|
||||
user_group_id: None,
|
||||
users: vec![].into(),
|
||||
instances: vec![].into(),
|
||||
users: vec![],
|
||||
instances: vec![],
|
||||
case_sensitive: true,
|
||||
notify: true,
|
||||
with_replies: false,
|
||||
|
|
|
@ -25,16 +25,16 @@
|
|||
"@tensorflow/tfjs-node": "3.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.2.0",
|
||||
"@bull-board/koa": "5.2.0",
|
||||
"@bull-board/ui": "5.2.0",
|
||||
"@bull-board/api": "5.6.0",
|
||||
"@bull-board/koa": "5.6.0",
|
||||
"@bull-board/ui": "5.6.0",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@elastic/elasticsearch": "7.17.0",
|
||||
"@koa/cors": "3.4.3",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "9.0.1",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@redocly/openapi-core": "1.0.0-beta.131",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@tensorflow/tfjs": "^4.2.0",
|
||||
|
@ -42,20 +42,19 @@
|
|||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autolinker": "4.0.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1277.0",
|
||||
"aws-sdk": "2.1413.0",
|
||||
"axios": "^1.4.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.10.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"calckey-js": "workspace:*",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.2.0",
|
||||
"chalk": "5.3.0",
|
||||
"chalk-template": "0.4.0",
|
||||
"chokidar": "3.5.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
|
@ -68,15 +67,16 @@
|
|||
"got": "12.5.3",
|
||||
"hpagent": "0.1.2",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.0.11",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "20.0.3",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.2.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"koa": "2.13.4",
|
||||
"jsrsasign": "10.8.6",
|
||||
"koa": "2.14.2",
|
||||
"koa-body": "^6.0.1",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
"koa-favicon": "2.1.0",
|
||||
"koa-json-body": "5.3.0",
|
||||
"koa-logger": "3.2.1",
|
||||
|
@ -98,9 +98,9 @@
|
|||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.1.2",
|
||||
"otpauth": "^9.1.3",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.0",
|
||||
"pg": "8.11.1",
|
||||
"private-ip": "2.3.4",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
@ -110,7 +110,7 @@
|
|||
"qs": "6.11.2",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.19.0",
|
||||
"re2": "1.19.1",
|
||||
"redis-lock": "0.1.4",
|
||||
"redis-semaphore": "5.3.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"rss-parser": "3.13.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "^3.0.5",
|
||||
"semver": "7.5.1",
|
||||
"semver": "7.5.4",
|
||||
"sharp": "0.32.1",
|
||||
"sonic-channel": "^1.3.1",
|
||||
"stringz": "2.1.0",
|
||||
|
@ -130,27 +130,26 @@
|
|||
"tinycolor2": "1.5.2",
|
||||
"tmp": "0.2.1",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typeorm": "0.3.17",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "9.0.0",
|
||||
"web-push": "3.6.1",
|
||||
"web-push": "3.6.3",
|
||||
"websocket": "1.0.34",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@swc/core": "^1.3.68",
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.4",
|
||||
"@types/koa": "2.13.5",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.9",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/koa": "2.13.6",
|
||||
"@types/koa-bodyparser": "4.3.10",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
|
@ -169,7 +168,7 @@
|
|||
"@types/probe-image-size": "^7.2.0",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/qrcode": "1.5.1",
|
||||
"@types/qs": "6.9.7",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
|
@ -177,29 +176,26 @@
|
|||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.5.0",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"@types/ws": "8.5.5",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint": "^8.44.0",
|
||||
"execa": "6.1.0",
|
||||
"json5": "2.2.3",
|
||||
"json5-loader": "4.0.1",
|
||||
"mocha": "10.2.0",
|
||||
"pug": "3.0.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"ts-loader": "9.4.3",
|
||||
"ts-loader": "9.4.4",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.1.3",
|
||||
"webpack": "^5.85.1",
|
||||
"typescript": "5.1.6",
|
||||
"webpack": "^5.88.1",
|
||||
"ws": "8.13.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import config from "@/config/index.js";
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||
import {
|
||||
DB_MAX_NOTE_TEXT_LENGTH,
|
||||
DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||
} from "@/misc/hard-limits.js";
|
||||
|
||||
export const MAX_NOTE_TEXT_LENGTH =
|
||||
config.maxNoteLength != null ? config.maxNoteLength : 3000; // <- should we increase this?
|
||||
export const MAX_NOTE_TEXT_LENGTH = Math.min(
|
||||
config.maxNoteLength ?? 3000,
|
||||
DB_MAX_NOTE_TEXT_LENGTH,
|
||||
);
|
||||
export const MAX_CAPTION_TEXT_LENGTH = Math.min(
|
||||
config.maxCaptionLength ?? 1500,
|
||||
DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||
|
|
|
@ -4,7 +4,7 @@ export type Acct = {
|
|||
};
|
||||
|
||||
export function parse(acct: string): Acct {
|
||||
if (acct.startsWith("@")) acct = acct.substr(1);
|
||||
if (acct.startsWith("@")) acct = acct.slice(1);
|
||||
const split = acct.split("@", 2);
|
||||
return { username: split[0], host: split[1] || null };
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
.stream(url, {
|
||||
headers: {
|
||||
"User-Agent": config.userAgent,
|
||||
Host: new URL(url).hostname,
|
||||
},
|
||||
timeout: {
|
||||
lookup: timeout,
|
||||
|
|
|
@ -3,8 +3,13 @@
|
|||
/**
|
||||
* Maximum note text length that can be stored in DB.
|
||||
* Surrogate pairs count as one
|
||||
*
|
||||
* NOTE: this can hypothetically be pushed further
|
||||
* (up to 250000000), but will likely cause truncations
|
||||
* and incompatibilities with other servers,
|
||||
* as well as potential performance issues.
|
||||
*/
|
||||
export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
|
||||
export const DB_MAX_NOTE_TEXT_LENGTH = 100000;
|
||||
|
||||
/**
|
||||
* Maximum image description length that can be stored in DB.
|
||||
|
|
|
@ -20,5 +20,9 @@ export function nyaize(text: string): string {
|
|||
)
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥")
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥")
|
||||
// el-GR
|
||||
.replaceAll("να", "νια")
|
||||
.replaceAll("ΝΑ", "ΝΙΑ")
|
||||
.replaceAll("Να", "Νια")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -326,13 +326,13 @@ export class Meta {
|
|||
public smtpPort: number | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public smtpUser: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public smtpPass: string | null;
|
||||
|
@ -556,4 +556,10 @@ export class Meta {
|
|||
default: true,
|
||||
})
|
||||
public enableIdenticonGeneration: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
nullable: true,
|
||||
})
|
||||
public donationLink: string | null;
|
||||
}
|
||||
|
|
|
@ -453,6 +453,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isAdmin: user.isAdmin || falsy,
|
||||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
isLocked: user.isLocked,
|
||||
isCat: user.isCat || falsy,
|
||||
speakAsCat: user.speakAsCat || falsy,
|
||||
instance: user.host
|
||||
|
@ -497,7 +498,6 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
: null,
|
||||
bannerBlurhash: user.banner?.blurhash || null,
|
||||
bannerColor: null, // 後方互換性のため
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: user.isSilenced || falsy,
|
||||
isSuspended: user.isSuspended || falsy,
|
||||
description: profile!.description,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type Bull from "bull";
|
||||
import type { DoneCallback } from "bull";
|
||||
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import { Notes } from "@/models/index.js";
|
||||
|
@ -11,7 +12,7 @@ const logger = queueLogger.createSubLogger("index-all-notes");
|
|||
|
||||
export default async function indexAllNotes(
|
||||
job: Bull.Job<Record<string, unknown>>,
|
||||
done: () => void,
|
||||
done: DoneCallback,
|
||||
): Promise<void> {
|
||||
logger.info("Indexing all notes...");
|
||||
|
||||
|
@ -20,7 +21,7 @@ export default async function indexAllNotes(
|
|||
let total: number = (job.data.total as number) ?? 0;
|
||||
|
||||
let running = true;
|
||||
const take = 100000;
|
||||
const take = 10000;
|
||||
const batch = 100;
|
||||
while (running) {
|
||||
logger.info(
|
||||
|
@ -41,13 +42,14 @@ export default async function indexAllNotes(
|
|||
},
|
||||
relations: ["user"],
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to query notes ${e}`);
|
||||
continue;
|
||||
done(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
job.progress(100);
|
||||
await job.progress(100);
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
@ -55,7 +57,7 @@ export default async function indexAllNotes(
|
|||
try {
|
||||
const count = await Notes.count();
|
||||
total = count;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
} catch (e) {}
|
||||
|
||||
for (let i = 0; i < notes.length; i += batch) {
|
||||
|
@ -69,12 +71,12 @@ export default async function indexAllNotes(
|
|||
|
||||
indexedCount += chunk.length;
|
||||
const pct = (indexedCount / total) * 100;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
job.progress(+pct.toFixed(1));
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
await job.progress(+pct.toFixed(1));
|
||||
logger.info(`Indexed notes ${indexedCount}/${total ? total : "?"}`);
|
||||
}
|
||||
cursor = notes[notes.length - 1].id;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
|
||||
if (notes.length < take) {
|
||||
running = false;
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as fs from "node:fs";
|
|||
import { queueLogger } from "../../logger.js";
|
||||
import { addFile } from "@/services/drive/add-file.js";
|
||||
import { format as dateFormat } from "date-fns";
|
||||
import { Users, Notes, Polls } from "@/models/index.js";
|
||||
import { Users, Notes, Polls, DriveFiles } from "@/models/index.js";
|
||||
import { MoreThan } from "typeorm";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { Poll } from "@/models/entities/poll.js";
|
||||
|
@ -75,7 +75,7 @@ export async function exportNotes(
|
|||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
}
|
||||
const content = JSON.stringify(serialize(note, poll));
|
||||
const content = JSON.stringify(await serialize(note, poll));
|
||||
const isFirst = exportedNotesCount === 0;
|
||||
await write(isFirst ? content : ",\n" + content);
|
||||
exportedNotesCount++;
|
||||
|
@ -112,15 +112,16 @@ export async function exportNotes(
|
|||
done();
|
||||
}
|
||||
|
||||
function serialize(
|
||||
async function serialize(
|
||||
note: Note,
|
||||
poll: Poll | null = null,
|
||||
): Record<string, unknown> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
id: note.id,
|
||||
text: note.text,
|
||||
createdAt: note.createdAt,
|
||||
fileIds: note.fileIds,
|
||||
files: await DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
poll: poll,
|
||||
|
|
|
@ -3,6 +3,8 @@ import create from "@/services/note/create.js";
|
|||
import { Users } from "@/models/index.js";
|
||||
import type { DbUserImportMastoPostJobData } from "@/queue/types.js";
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type Bull from "bull";
|
||||
|
||||
const logger = queueLogger.createSubLogger("import-calckey-post");
|
||||
|
@ -29,10 +31,25 @@ export async function importCkPost(
|
|||
done();
|
||||
return;
|
||||
}
|
||||
const urls = (post.files || [])
|
||||
.map((x: any) => x.url)
|
||||
.filter((x: String) => x.startsWith("http"));
|
||||
const files: DriveFile[] = [];
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const file = await uploadFromUrl({
|
||||
url: url,
|
||||
user: user,
|
||||
});
|
||||
files.push(file);
|
||||
} catch (e) {
|
||||
logger.error(`Skipped adding file to drive: ${url}`);
|
||||
}
|
||||
}
|
||||
const { text, cw, localOnly, createdAt } = Post.parse(post);
|
||||
const note = await create(user, {
|
||||
createdAt: createdAt,
|
||||
files: undefined,
|
||||
files: files.length == 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply: null,
|
||||
|
|
|
@ -6,6 +6,8 @@ import type Bull from "bull";
|
|||
import { htmlToMfm } from "@/remote/activitypub/misc/html-to-mfm.js";
|
||||
import { resolveNote } from "@/remote/activitypub/models/note.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
|
||||
const logger = queueLogger.createSubLogger("import-masto-post");
|
||||
|
||||
|
@ -43,14 +45,30 @@ export async function importMastoPost(
|
|||
throw e;
|
||||
}
|
||||
job.progress(80);
|
||||
const urls = post.object.attachment
|
||||
.map((x: any) => x.url)
|
||||
.filter((x: String) => x.startsWith("http"));
|
||||
const files: DriveFile[] = [];
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const file = await uploadFromUrl({
|
||||
url: url,
|
||||
user: user,
|
||||
});
|
||||
files.push(file);
|
||||
} catch (e) {
|
||||
logger.error(`Skipped adding file to drive: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
const note = await create(user, {
|
||||
createdAt: new Date(post.object.published),
|
||||
files: undefined,
|
||||
files: files.length == 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply,
|
||||
renote: null,
|
||||
cw: post.sensitive,
|
||||
cw: post.object.sensitive ? post.object.summary : undefined,
|
||||
localOnly: false,
|
||||
visibility: "hidden",
|
||||
visibleUsers: [],
|
||||
|
|
|
@ -147,11 +147,11 @@ export async function fetchPerson(
|
|||
}
|
||||
|
||||
//#region Returns if already registered with this server
|
||||
const exist = await Users.findOneBy({ uri });
|
||||
const user = await Users.findOneBy({ uri });
|
||||
|
||||
if (exist) {
|
||||
await uriPersonCache.set(uri, exist);
|
||||
return exist;
|
||||
if (user != null) {
|
||||
await uriPersonCache.set(uri, user);
|
||||
return user;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
@ -396,9 +396,9 @@ export async function updatePerson(
|
|||
}
|
||||
|
||||
//#region Already registered on this server?
|
||||
const exist = (await Users.findOneBy({ uri })) as IRemoteUser;
|
||||
const user = (await Users.findOneBy({ uri })) as IRemoteUser;
|
||||
|
||||
if (exist == null) {
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
@ -416,17 +416,15 @@ export async function updatePerson(
|
|||
[person.icon, person.image].map((img) =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(exist, img).catch(() => null),
|
||||
: resolveImage(user, img).catch(() => null),
|
||||
),
|
||||
);
|
||||
|
||||
// Custom pictogram acquisition
|
||||
const emojis = await extractEmojis(person.tag || [], exist.host).catch(
|
||||
(e) => {
|
||||
const emojis = await extractEmojis(person.tag || [], user.host).catch((e) => {
|
||||
logger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map((emoji) => emoji.name);
|
||||
|
||||
|
@ -518,11 +516,11 @@ export async function updatePerson(
|
|||
}
|
||||
|
||||
// Update user
|
||||
await Users.update(exist.id, updates);
|
||||
await Users.update(user.id, updates);
|
||||
|
||||
if (person.publicKey) {
|
||||
await UserPublickeys.update(
|
||||
{ userId: exist.id },
|
||||
{ userId: user.id },
|
||||
{
|
||||
keyId: person.publicKey.id,
|
||||
keyPem: person.publicKey.publicKeyPem,
|
||||
|
@ -531,7 +529,7 @@ export async function updatePerson(
|
|||
}
|
||||
|
||||
await UserProfiles.update(
|
||||
{ userId: exist.id },
|
||||
{ userId: user.id },
|
||||
{
|
||||
url: url,
|
||||
fields,
|
||||
|
@ -543,15 +541,15 @@ export async function updatePerson(
|
|||
},
|
||||
);
|
||||
|
||||
publishInternalEvent("remoteUserUpdated", { id: exist.id });
|
||||
publishInternalEvent("remoteUserUpdated", { id: user.id });
|
||||
|
||||
// Hashtag Update
|
||||
updateUsertags(exist, tags);
|
||||
updateUsertags(user, tags);
|
||||
|
||||
// If the user in question is a follower, followers will also be updated.
|
||||
await Followings.update(
|
||||
{
|
||||
followerId: exist.id,
|
||||
followerId: user.id,
|
||||
},
|
||||
{
|
||||
followerSharedInbox:
|
||||
|
@ -560,7 +558,7 @@ export async function updatePerson(
|
|||
},
|
||||
);
|
||||
|
||||
await updateFeatured(exist.id, resolver).catch((err) => logger.error(err));
|
||||
await updateFeatured(user.id, resolver).catch((err) => logger.error(err));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -576,10 +574,10 @@ export async function resolvePerson(
|
|||
if (typeof uri !== "string") throw new Error("uri is not string");
|
||||
|
||||
//#region If already registered on this server, return it.
|
||||
const exist = await fetchPerson(uri);
|
||||
const user = await fetchPerson(uri);
|
||||
|
||||
if (exist) {
|
||||
return exist;
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
@ -491,6 +491,11 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
donationLink: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -604,5 +609,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
experimentalFeatures: instance.experimentalFeatures,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
donationLink: instance.donationLink,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -40,9 +40,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
const exist = await PromoNotes.findOneBy({ noteId: note.id });
|
||||
const exist = await PromoNotes.exist({ where: { noteId: note.id } });
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyPromoted);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Meta } from "@/models/entities/meta.js";
|
||||
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from "@/misc/hard-limits.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import define from "../../define.js";
|
||||
|
||||
|
@ -177,6 +176,9 @@ export const paramDef = {
|
|||
postImports: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
enableServerMachineStats: { type: "boolean" },
|
||||
enableIdenticonGeneration: { type: "boolean" },
|
||||
donationLink: { type: "string", nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -218,6 +220,15 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
if (Array.isArray(ps.recommendedInstances)) {
|
||||
set.recommendedInstances = ps.recommendedInstances.filter(Boolean);
|
||||
if (set.recommendedInstances?.length > 0) {
|
||||
set.recommendedInstances.forEach((instance, index) => {
|
||||
if (/^https?:\/\//i.test(instance)) {
|
||||
set.recommendedInstances![index] = instance
|
||||
.replace(/^https?:\/\//i, "")
|
||||
.replace(/\/$/, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.hiddenTags)) {
|
||||
|
@ -568,6 +579,21 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.experimentalFeatures = ps.experimentalFeatures || undefined;
|
||||
}
|
||||
|
||||
if (ps.enableServerMachineStats !== undefined) {
|
||||
set.enableServerMachineStats = ps.enableServerMachineStats;
|
||||
}
|
||||
|
||||
if (ps.enableIdenticonGeneration !== undefined) {
|
||||
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
|
||||
}
|
||||
|
||||
if (ps.donationLink !== undefined) {
|
||||
set.donationLink = ps.donationLink;
|
||||
if (set.donationLink && !/^https?:\/\//i.test(set.donationLink)) {
|
||||
set.donationLink = `https://${set.donationLink}`;
|
||||
}
|
||||
}
|
||||
|
||||
await db.transaction(async (transactionalEntityManager) => {
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
order: {
|
||||
|
|
|
@ -41,12 +41,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const accessToken = secureRndstr(32, true);
|
||||
|
||||
// Fetch exist access token
|
||||
const exist = await AccessTokens.findOneBy({
|
||||
const exist = await AccessTokens.exist({
|
||||
where: {
|
||||
appId: session.appId,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
// Lookup app
|
||||
const app = await Apps.findOneByOrFail({ id: session.appId });
|
||||
|
||||
|
|
|
@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await Blockings.findOneBy({
|
||||
const exist = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await Blockings.findOneBy({
|
||||
const exist = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
|
||||
|
|
|
@ -57,12 +57,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
const exist = await ClipNotes.findOneBy({
|
||||
const exist = await ClipNotes.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyClipped);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,10 +26,12 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const file = await DriveFiles.findOneBy({
|
||||
const exist = await DriveFiles.exist({
|
||||
where: {
|
||||
md5: ps.md5,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return file != null;
|
||||
return exist;
|
||||
});
|
||||
|
|
|
@ -82,12 +82,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFollowing);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,12 +40,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await GalleryLikes.findOneBy({
|
||||
const exist = await GalleryLikes.exist({
|
||||
where: {
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,17 +38,17 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.noSuchPost);
|
||||
}
|
||||
|
||||
const exist = await GalleryLikes.findOneBy({
|
||||
const like = await GalleryLikes.findOneBy({
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (like == null) {
|
||||
throw new ApiError(meta.errors.notLiked);
|
||||
}
|
||||
|
||||
// Delete like
|
||||
await GalleryLikes.delete(exist.id);
|
||||
await GalleryLikes.delete(like.id);
|
||||
|
||||
GalleryPosts.decrement({ id: post.id }, "likedCount", 1);
|
||||
});
|
||||
|
|
|
@ -30,19 +30,23 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// Check if announcement exists
|
||||
const announcement = await Announcements.findOneBy({ id: ps.announcementId });
|
||||
const exist = await Announcements.exist({
|
||||
where: { id: ps.announcementId },
|
||||
});
|
||||
|
||||
if (announcement == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
}
|
||||
|
||||
// Check if already read
|
||||
const read = await AnnouncementReads.findOneBy({
|
||||
const read = await AnnouncementReads.exist({
|
||||
where: {
|
||||
announcementId: ps.announcementId,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (read != null) {
|
||||
if (read) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const token = await AccessTokens.findOneBy({ id: ps.tokenId });
|
||||
const exist = await AccessTokens.exist({ where: { id: ps.tokenId } });
|
||||
|
||||
if (token) {
|
||||
if (exist) {
|
||||
await AccessTokens.delete({
|
||||
id: ps.tokenId,
|
||||
userId: user.id,
|
||||
|
|
|
@ -389,6 +389,11 @@ export const meta = {
|
|||
nullable: false,
|
||||
default: "⭐",
|
||||
},
|
||||
donationLink: {
|
||||
type: "string",
|
||||
optional: "true",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -491,6 +496,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
translatorAvailable:
|
||||
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
|
||||
defaultReaction: instance.defaultReaction,
|
||||
donationLink: instance.donationLink,
|
||||
|
||||
...(ps.detail
|
||||
? {
|
||||
|
|
|
@ -64,12 +64,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await Mutings.findOneBy({
|
||||
const exist = await Mutings.exist({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,18 +56,18 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not muting
|
||||
const exist = await Mutings.findOneBy({
|
||||
const muting = await Mutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (muting == null) {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await Mutings.delete({
|
||||
id: exist.id,
|
||||
id: muting.id,
|
||||
});
|
||||
|
||||
publishUserEvent(user.id, "unmute", mutee);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Brackets } from "typeorm";
|
||||
import { Notes } from "@/models/index.js";
|
||||
import define from "../../define.js";
|
||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
|
@ -11,6 +10,7 @@ export const meta = {
|
|||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
description: "Get threaded/chained replies to a note",
|
||||
|
||||
res: {
|
||||
type: "array",
|
||||
|
@ -23,13 +23,14 @@ export const meta = {
|
|||
ref: "Note",
|
||||
},
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
noteId: { type: "string", format: "misskey:id" },
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
||||
depth: { type: "integer", minimum: 1, maximum: 100, default: 12 },
|
||||
sinceId: { type: "string", format: "misskey:id" },
|
||||
untilId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
description: "Get conversation of a note thread/chain by a reply",
|
||||
|
||||
res: {
|
||||
type: "array",
|
||||
|
@ -34,7 +35,11 @@ export const meta = {
|
|||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
noteId: { type: "string", format: "misskey:id" },
|
||||
noteId: {
|
||||
type: "string",
|
||||
format: "misskey:id",
|
||||
description: "Should be a reply",
|
||||
},
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: "integer", default: 0 },
|
||||
},
|
||||
|
@ -51,7 +56,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const conversation: Note[] = [];
|
||||
let i = 0;
|
||||
|
||||
async function get(id: any) {
|
||||
async function get(id: string) {
|
||||
i++;
|
||||
const p = await getNote(id, user).catch((e) => {
|
||||
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null;
|
||||
|
@ -60,7 +65,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
if (p == null) return;
|
||||
|
||||
if (i > ps.offset!) {
|
||||
if (i > ps.offset) {
|
||||
conversation.push(p);
|
||||
}
|
||||
|
||||
|
|
|
@ -224,11 +224,13 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
// Check blocking
|
||||
if (renote.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const isBlocked = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: renote.userId,
|
||||
blockeeId: user.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (isBlocked) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
@ -249,11 +251,13 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
// Check blocking
|
||||
if (reply.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const isBlocked = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: reply.userId,
|
||||
blockeeId: user.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (isBlocked) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,12 +43,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await NoteFavorites.findOneBy({
|
||||
const exist = await NoteFavorites.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,15 +42,15 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await NoteFavorites.findOneBy({
|
||||
const favorite = await NoteFavorites.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (favorite == null) {
|
||||
throw new ApiError(meta.errors.notFavorited);
|
||||
}
|
||||
|
||||
// Delete favorite
|
||||
await NoteFavorites.delete(exist.id);
|
||||
await NoteFavorites.delete(favorite.id);
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ export const meta = {
|
|||
message: "No such note.",
|
||||
code: "NO_SUCH_NOTE",
|
||||
id: "24fcbfc6-2e37-42b6-8388-c29b3861a08d",
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -40,12 +40,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await PageLikes.findOneBy({
|
||||
const exist = await PageLikes.exist({
|
||||
where: {
|
||||
pageId: page.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,17 +38,17 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.noSuchPage);
|
||||
}
|
||||
|
||||
const exist = await PageLikes.findOneBy({
|
||||
const like = await PageLikes.findOneBy({
|
||||
pageId: page.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (like == null) {
|
||||
throw new ApiError(meta.errors.notLiked);
|
||||
}
|
||||
|
||||
// Delete like
|
||||
await PageLikes.delete(exist.id);
|
||||
await PageLikes.delete(like.id);
|
||||
|
||||
Pages.decrement({ id: page.id }, "likedCount", 1);
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ const _dirname = dirname(_filename);
|
|||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
description: "Get list of Calckey patrons from Codeberg",
|
||||
description: "Get Calckey patrons",
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: false,
|
||||
|
@ -51,6 +51,8 @@ export default define(meta, paramDef, async (ps) => {
|
|||
});
|
||||
await redisClient.set("patrons", JSON.stringify(patrons), "EX", 3600);
|
||||
}
|
||||
|
||||
return patrons["patrons"];
|
||||
return {
|
||||
patrons: patrons["patrons"],
|
||||
sponsors: patrons["sponsors"],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -33,12 +33,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
const exist = await PromoReads.findOneBy({
|
||||
const exist = await PromoReads.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,12 +47,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await RenoteMutings.findOneBy({
|
||||
const exist = await RenoteMutings.exist({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,18 +45,18 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not muting
|
||||
const exist = await RenoteMutings.findOneBy({
|
||||
const muting = await RenoteMutings.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (muting == null) {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await RenoteMutings.delete({
|
||||
id: exist.id,
|
||||
id: muting.id,
|
||||
});
|
||||
|
||||
// publishUserEvent(user.id, "unmute", mutee);
|
||||
|
|
|
@ -57,8 +57,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
// if already subscribed
|
||||
const exist = await SwSubscriptions.findOneBy({
|
||||
const subscription = await SwSubscriptions.findOneBy({
|
||||
userId: me.id,
|
||||
endpoint: ps.endpoint,
|
||||
auth: ps.auth,
|
||||
|
@ -67,13 +66,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
if (exist != null) {
|
||||
// if already subscribed
|
||||
if (subscription != null) {
|
||||
return {
|
||||
state: "already-subscribed" as const,
|
||||
key: instance.swPublicKey,
|
||||
userId: me.id,
|
||||
endpoint: exist.endpoint,
|
||||
sendReadMessage: exist.sendReadMessage,
|
||||
endpoint: subscription.endpoint,
|
||||
sendReadMessage: subscription.sendReadMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -42,16 +42,16 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const exist = await SwSubscriptions.findOneBy({
|
||||
const subscription = await SwSubscriptions.findOneBy({
|
||||
userId: me.id,
|
||||
endpoint: ps.endpoint,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (subscription != null) {
|
||||
return {
|
||||
userId: exist.userId,
|
||||
endpoint: exist.endpoint,
|
||||
sendReadMessage: exist.sendReadMessage,
|
||||
userId: subscription.userId,
|
||||
endpoint: subscription.endpoint,
|
||||
sendReadMessage: subscription.sendReadMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -98,11 +98,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await Followings.findOneBy({
|
||||
const isFollowed = await Followings.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (following == null) {
|
||||
if (!isFollowed) {
|
||||
throw new ApiError(meta.errors.nullFollowers);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,11 +97,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await Followings.findOneBy({
|
||||
const isFollowing = await Followings.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (following == null) {
|
||||
if (!isFollowing) {
|
||||
throw new ApiError(meta.errors.cannot_find);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,12 +52,14 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOneBy({
|
||||
const listExists = await UserLists.exist({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
if (!listExists) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
|
@ -70,18 +72,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
// Check blocking
|
||||
if (user.id !== me.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const isBlocked = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: user.id,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (isBlocked) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await UserListJoinings.findOneBy({
|
||||
const exist = await UserListJoinings.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
|
|
|
@ -37,12 +37,14 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOneBy({
|
||||
const exist = await UserLists.exist({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId });
|
||||
|
||||
if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
|
||||
if (me.id !== ps.userId && !profile.publicReactions) {
|
||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Entity } from "megalodon";
|
||||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, Notes } from "@/models/index.js";
|
||||
import { IsNull, MoreThan } from "typeorm";
|
||||
|
@ -17,7 +18,7 @@ export async function getInstance(response: Entity.Instance) {
|
|||
response.description ||
|
||||
"This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
|
||||
email: response.email || "",
|
||||
version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it.
|
||||
version: `3.0.0 (compatible; Calckey ${config.version})`,
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: await totalUsers,
|
||||
|
|
|
@ -67,6 +67,25 @@ export function apiStatusMastodon(router: Router): void {
|
|||
const { sensitive } = body;
|
||||
body.sensitive =
|
||||
typeof sensitive === "string" ? sensitive === "true" : sensitive;
|
||||
|
||||
if (body.poll) {
|
||||
if (
|
||||
body.poll.expires_in != null &&
|
||||
typeof body.poll.expires_in === "string"
|
||||
)
|
||||
body.poll.expires_in = parseInt(body.poll.expires_in);
|
||||
if (
|
||||
body.poll.multiple != null &&
|
||||
typeof body.poll.multiple === "string"
|
||||
)
|
||||
body.poll.multiple = body.poll.multiple == "true";
|
||||
if (
|
||||
body.poll.hide_totals != null &&
|
||||
typeof body.poll.hide_totals === "string"
|
||||
)
|
||||
body.poll.hide_totals = body.poll.hide_totals == "true";
|
||||
}
|
||||
|
||||
const data = await client.postStatus(text, body);
|
||||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
|
@ -86,7 +105,7 @@ export function apiStatusMastodon(router: Router): void {
|
|||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.status = ctx.status == 404 ? 404 : 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ function getUserToken(ctx: Koa.BaseContext): string | null {
|
|||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? (url.endsWith("/") ? url.substr(0, url.length - 1) : url) : "";
|
||||
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
|
||||
}
|
||||
|
||||
const referer = ctx.headers["referer"];
|
||||
|
|
|
@ -18,7 +18,7 @@ function getUserToken(ctx: Koa.BaseContext): string | null {
|
|||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? (url.endsWith("/") ? url.substr(0, url.length - 1) : url) : "";
|
||||
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
|
||||
}
|
||||
|
||||
const referer = ctx.headers["referer"];
|
||||
|
|
|
@ -22,11 +22,13 @@ export default class extends Channel {
|
|||
this.listId = params.listId as string;
|
||||
|
||||
// Check existence and owner
|
||||
const list = await UserLists.findOneBy({
|
||||
const exist = await UserLists.exist({
|
||||
where: {
|
||||
id: this.listId,
|
||||
userId: this.user!.id,
|
||||
},
|
||||
});
|
||||
if (!list) return;
|
||||
if (!exist) return;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on(`userListStream:${this.listId}`, this.send);
|
||||
|
|
|
@ -247,7 +247,7 @@ export default class Connection {
|
|||
|
||||
for (const obj of objs) {
|
||||
const { type, body } = obj;
|
||||
console.log(type, body);
|
||||
// console.log(type, body);
|
||||
switch (type) {
|
||||
case "readNotification":
|
||||
this.onReadNotification(body);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import { promises } from "node:dns";
|
||||
import type Koa from "koa";
|
||||
import sharp from "sharp";
|
||||
import type { IImage } from "@/services/drive/image-processor.js";
|
||||
|
@ -19,6 +21,40 @@ export async function proxyMedia(ctx: Koa.Context) {
|
|||
return;
|
||||
}
|
||||
|
||||
const { hostname } = new URL(url);
|
||||
let resolvedIps;
|
||||
try {
|
||||
resolvedIps = await promises.resolve(hostname);
|
||||
} catch (error) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { message: "Invalid URL" };
|
||||
return;
|
||||
}
|
||||
|
||||
const isSSRF = resolvedIps.some((ip) => {
|
||||
if (net.isIPv4(ip)) {
|
||||
const parts = ip.split(".").map(Number);
|
||||
return (
|
||||
parts[0] === 10 ||
|
||||
(parts[0] === 172 && parts[1] >= 16 && parts[1] < 32) ||
|
||||
(parts[0] === 192 && parts[1] === 168) ||
|
||||
parts[0] === 127 ||
|
||||
parts[0] === 0
|
||||
);
|
||||
} else if (net.isIPv6(ip)) {
|
||||
return (
|
||||
ip.startsWith("::") || ip.startsWith("fc00:") || ip.startsWith("fe80:")
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isSSRF) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { message: "Access to this URL is not allowed" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
|
|
|
@ -102,7 +102,11 @@
|
|||
localStorage.setItem("fontSize", null);
|
||||
fontSize = localStorage.getItem("fontSize");
|
||||
}
|
||||
document.documentElement.style.fontSize = fontSize + "px";
|
||||
document.documentElement.style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
if (["ja-JP", "ja-KS", "ko-KR", "zh-CN", "zh-TW"].includes(lang)) {
|
||||
document.documentElement.classList.add("useCJKFont");
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem("useSystemFont");
|
||||
|
@ -123,7 +127,7 @@
|
|||
}
|
||||
|
||||
async function addStyle(styleText) {
|
||||
let css = document.createElement("style");
|
||||
const css = document.createElement("style");
|
||||
css.appendChild(document.createTextNode(styleText));
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
|
|
|
@ -13,13 +13,13 @@ export default async (
|
|||
user: { id: User["id"]; host: User["host"] },
|
||||
note: Note,
|
||||
) => {
|
||||
// if already unreacted
|
||||
const exist = await NoteReactions.findOneBy({
|
||||
const reaction = await NoteReactions.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
// if already unreacted
|
||||
if (reaction == null) {
|
||||
throw new IdentifiableError(
|
||||
"60527ec9-b4cb-4a88-a6bd-32d3ad26817d",
|
||||
"not reacted",
|
||||
|
@ -27,7 +27,7 @@ export default async (
|
|||
}
|
||||
|
||||
// Delete reaction
|
||||
const result = await NoteReactions.delete(exist.id);
|
||||
const result = await NoteReactions.delete(reaction.id);
|
||||
|
||||
if (result.affected !== 1) {
|
||||
throw new IdentifiableError(
|
||||
|
@ -37,7 +37,7 @@ export default async (
|
|||
}
|
||||
|
||||
// Decrement reactions count
|
||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
const sql = `jsonb_set("reactions", '{${reaction.reaction}}', (COALESCE("reactions"->>'${reaction.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
await Notes.createQueryBuilder()
|
||||
.update()
|
||||
.set({
|
||||
|
@ -49,14 +49,14 @@ export default async (
|
|||
Notes.decrement({ id: note.id }, "score", 1);
|
||||
|
||||
publishNoteStream(note.id, "unreacted", {
|
||||
reaction: decodeReaction(exist.reaction).reaction,
|
||||
reaction: decodeReaction(reaction.reaction).reaction,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
//#region 配信
|
||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||
const content = renderActivity(
|
||||
renderUndo(await renderLike(exist, note), user),
|
||||
renderUndo(await renderLike(reaction, note), user),
|
||||
);
|
||||
const dm = new DeliverManager(user, content);
|
||||
if (note.userHost !== null) {
|
||||
|
|
|
@ -42,9 +42,9 @@ export async function insertNoteUnread(
|
|||
|
||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||
setTimeout(async () => {
|
||||
const exist = await NoteUnreads.findOneBy({ id: unread.id });
|
||||
const exist = await NoteUnreads.exist({ where: { id: unread.id } });
|
||||
|
||||
if (exist == null) return;
|
||||
if (!exist) return;
|
||||
|
||||
if (params.isMentioned) {
|
||||
publishMainStream(userId, "unreadMention", note.id);
|
||||
|
|
|
@ -4,7 +4,7 @@ export type Acct = {
|
|||
};
|
||||
|
||||
export function parse(acct: string): Acct {
|
||||
if (acct.startsWith("@")) acct = acct.substr(1);
|
||||
if (acct.startsWith("@")) acct = acct.slice(1);
|
||||
const split = acct.split("@", 2);
|
||||
return { username: split[0], host: split[1] || null };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"],
|
||||
"plugins": ["file-progress", "prettier"],
|
||||
"rules": {
|
||||
"file-progress/activate": 1
|
||||
}
|
||||
}
|
|
@ -4,11 +4,14 @@
|
|||
"scripts": {
|
||||
"watch": "pnpm vite build --watch --mode development",
|
||||
"build": "pnpm vite build",
|
||||
"lint": "pnpm rome check \"src/**/*.{ts,vue}\"",
|
||||
"format": "pnpm rome format * --write && pnpm prettier --write '**/*.{scss,vue}'"
|
||||
"lint": "pnpm rome check **/*.ts --apply && pnpm run lint:vue",
|
||||
"lint:vue": "pnpm paralint --ext .vue --fix '**/*.vue' --cache",
|
||||
"format": "pnpm rome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@eslint-sets/eslint-config-vue3": "^5.6.1",
|
||||
"@eslint-sets/eslint-config-vue3-ts": "^3.3.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
|
@ -16,7 +19,7 @@
|
|||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/gulp": "4.0.11",
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/katex": "0.16.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
|
@ -29,8 +32,8 @@
|
|||
"@vue/compiler-sfc": "3.3.4",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.2",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.19.1",
|
||||
"blurhash": "2.0.5",
|
||||
"broadcast-channel": "5.1.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer",
|
||||
"calckey-js": "workspace:*",
|
||||
"chart.js": "4.3.0",
|
||||
|
@ -39,56 +42,60 @@
|
|||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"city-timezones": "^1.2.1",
|
||||
"compare-versions": "5.0.3",
|
||||
"compare-versions": "6.0.0",
|
||||
"cropperjs": "2.0.0-beta.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"date-fns": "2.30.0",
|
||||
"emojilib": "github:thatonecalculator/emojilib",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"focus-trap": "^7.4.3",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-file-progress": "^1.3.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
"fast-blurhash": "^1.1.2",
|
||||
"focus-trap": "^7.5.2",
|
||||
"focus-trap-vue": "^4.0.2",
|
||||
"gsap": "^3.11.5",
|
||||
"gsap": "^3.12.2",
|
||||
"idb-keyval": "6.2.1",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.3",
|
||||
"katex": "0.16.7",
|
||||
"katex": "0.16.8",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"photoswipe": "5.3.7",
|
||||
"paralint": "^1.2.1",
|
||||
"photoswipe": "5.3.8",
|
||||
"prettier": "3.0.0",
|
||||
"prettier-plugin-vue": "1.1.6",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.1.1",
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.23.1",
|
||||
"rollup": "3.26.2",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.62.1",
|
||||
"sass": "1.63.6",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"swiper": "9.3.2",
|
||||
"swiper": "10.0.4",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.146.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tsc-alias": "1.8.6",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.7",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.1.3",
|
||||
"typescript": "5.1.6",
|
||||
"unicode-emoji-json": "^0.4.0",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.3.9",
|
||||
"vite": "4.4.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "3.3.4",
|
||||
"vue-draggable-plus": "^0.2.2",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
"vue-plyr": "^7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.1.0"
|
||||
"vue-prism-editor": "2.0.0-alpha.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,11 +80,11 @@ const emit = defineEmits<{
|
|||
(ev: "resolved", reportId: string): void;
|
||||
}>();
|
||||
|
||||
let forward = $ref(props.report.forwarded);
|
||||
const forward = $ref(props.report.forwarded);
|
||||
|
||||
function resolve() {
|
||||
os.apiWithDialog("admin/resolve-abuse-user-report", {
|
||||
forward: forward,
|
||||
forward,
|
||||
reportId: props.report.id,
|
||||
}).then(() => {
|
||||
emit("resolved", props.report.id);
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import * as Misskey from "calckey-js";
|
||||
import type * as Misskey from "calckey-js";
|
||||
import XWindow from "@/components/MkWindow.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
|
|
|
@ -109,12 +109,12 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
shallowRef,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
} from "vue";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { globalEvents } from "@/events.js";
|
||||
|
@ -173,21 +173,21 @@ const texts = computed(() => {
|
|||
return angles;
|
||||
});
|
||||
|
||||
let enabled = true;
|
||||
let majorGraduationColor = $ref<string>();
|
||||
let enabled = true,
|
||||
majorGraduationColor = $ref<string>(),
|
||||
// let minorGraduationColor = $ref<string>();
|
||||
let sHandColor = $ref<string>();
|
||||
let mHandColor = $ref<string>();
|
||||
let hHandColor = $ref<string>();
|
||||
let nowColor = $ref<string>();
|
||||
let h = $ref<number>(0);
|
||||
let m = $ref<number>(0);
|
||||
let s = $ref<number>(0);
|
||||
let hAngle = $ref<number>(0);
|
||||
let mAngle = $ref<number>(0);
|
||||
let sAngle = $ref<number>(0);
|
||||
let disableSAnimate = $ref(false);
|
||||
let sOneRound = false;
|
||||
sHandColor = $ref<string>(),
|
||||
mHandColor = $ref<string>(),
|
||||
hHandColor = $ref<string>(),
|
||||
nowColor = $ref<string>(),
|
||||
h = $ref<number>(0),
|
||||
m = $ref<number>(0),
|
||||
s = $ref<number>(0),
|
||||
hAngle = $ref<number>(0),
|
||||
mAngle = $ref<number>(0),
|
||||
sAngle = $ref<number>(0),
|
||||
disableSAnimate = $ref(false),
|
||||
sOneRound = false;
|
||||
|
||||
function tick() {
|
||||
const now = new Date();
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
||||
<p v-else>{{ title }}</p>
|
||||
</div>
|
||||
<div :class="$style.time">
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
<div v-if="announcement.updatedAt">
|
||||
{{ i18n.ts.updatedAt }}:
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
</div>
|
||||
</div>
|
||||
<Mfm :text="text" />
|
||||
<img
|
||||
v-if="imageUrl != null"
|
||||
|
@ -68,6 +75,10 @@ const gotIt = () => {
|
|||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
|
|
@ -85,11 +85,11 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
markRaw,
|
||||
ref,
|
||||
onUpdated,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import contains from "@/scripts/contains";
|
||||
|
@ -99,17 +99,17 @@ import { acct } from "@/filters/user";
|
|||
import * as os from "@/os";
|
||||
import { MFM_TAGS } from "@/scripts/mfm-tags";
|
||||
import { defaultStore } from "@/store";
|
||||
import { emojilist, addSkinTone } from "@/scripts/emojilist";
|
||||
import { addSkinTone, emojilist } from "@/scripts/emojilist";
|
||||
import { instance } from "@/instance";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
type EmojiDef = {
|
||||
interface EmojiDef {
|
||||
emoji: string;
|
||||
name: string;
|
||||
aliasOf?: string;
|
||||
url?: string;
|
||||
isCustomEmoji?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const lib = emojilist.filter((x) => x.category !== "flags");
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ const emit = defineEmits<{
|
|||
(ev: "click", payload: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
let el = $ref<HTMLElement | null>(null);
|
||||
let ripples = $ref<HTMLElement | null>(null);
|
||||
const el = $ref<HTMLElement | null>(null);
|
||||
const ripples = $ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus) {
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
type Captcha = {
|
||||
interface Captcha {
|
||||
render(
|
||||
container: string | Node,
|
||||
options: {
|
||||
|
@ -31,7 +31,7 @@ type Captcha = {
|
|||
execute(id: string): void;
|
||||
reset(id?: string): void;
|
||||
getResponse(id: string): string;
|
||||
};
|
||||
}
|
||||
|
||||
type CaptchaProvider = "hcaptcha" | "recaptcha";
|
||||
|
||||
|
@ -105,7 +105,7 @@ function requestRender() {
|
|||
captcha.value.render(captchaEl.value, {
|
||||
sitekey: props.sitekey,
|
||||
theme: defaultStore.state.darkMode ? "dark" : "light",
|
||||
callback: callback,
|
||||
callback,
|
||||
"expired-callback": callback,
|
||||
"error-callback": callback,
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue