refactor: use PGroonga for full-text search and remove support of other engines
Co-authored-by: tamaina <tamaina@hotmail.co.jp> Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
parent
eab42ceae5
commit
48e5d9de71
58 changed files with 182 additions and 1504 deletions
|
@ -85,28 +85,6 @@ redis:
|
|||
#prefix: example-prefix
|
||||
#db: 1
|
||||
|
||||
# Please configure either MeiliSearch *or* Sonic.
|
||||
# If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence.
|
||||
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────────────
|
||||
#meilisearch:
|
||||
# host: meilisearch
|
||||
# port: 7700
|
||||
# ssl: false
|
||||
# apiKey:
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Sonic configuration └─────────────────────────────────────
|
||||
|
||||
#sonic:
|
||||
# host: localhost
|
||||
# port: 1491
|
||||
# auth: SecretPassword
|
||||
# collection: notes
|
||||
# bucket: default
|
||||
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
|
|
|
@ -59,10 +59,6 @@ If you have access to a server that supports one of the sources below, I recomme
|
|||
### Optional dependencies
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) for video transcoding
|
||||
- Full text search (one of the following)
|
||||
- [Sonic](https://crates.io/crates/sonic-server)
|
||||
- [MeiliSearch](https://www.meilisearch.com/)
|
||||
- [ElasticSearch](https://www.elastic.co/elasticsearch/)
|
||||
- Caching server (one of the following)
|
||||
- [DragonflyDB](https://www.dragonflydb.io/) (recommended)
|
||||
- [KeyDB](https://keydb.dev/)
|
||||
|
|
|
@ -6,10 +6,12 @@ services:
|
|||
ports:
|
||||
- "26379:6379"
|
||||
db:
|
||||
image: docker.io/postgres:16-alpine
|
||||
image: docker.io/groonga/pgroonga:latest-alpine-16-slim
|
||||
environment:
|
||||
- "POSTGRES_PASSWORD=password"
|
||||
- "POSTGRES_USER=firefish"
|
||||
- "POSTGRES_DB=firefish_db"
|
||||
ports:
|
||||
- "25432:5432"
|
||||
volumes:
|
||||
- "./install.sql:/docker-entrypoint-initdb.d/install.sql:ro"
|
||||
|
|
1
dev/install.sql
Normal file
1
dev/install.sql
Normal file
|
@ -0,0 +1 @@
|
|||
CREATE EXTENSION pgroonga;
|
|
@ -8,9 +8,6 @@ services:
|
|||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
### Uncomment one of the following to use a search engine
|
||||
# - meilisearch
|
||||
# - sonic
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
|
@ -34,7 +31,7 @@ services:
|
|||
|
||||
db:
|
||||
restart: unless-stopped
|
||||
image: docker.io/postgres:16-alpine
|
||||
image: docker.io/groonga/pgroonga:latest-alpine-16-slim
|
||||
container_name: firefish_db
|
||||
networks:
|
||||
- calcnet
|
||||
|
@ -43,33 +40,6 @@ services:
|
|||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
|
||||
### Only one of the below should be used.
|
||||
### Meilisearch is better overall, but resource-intensive. Sonic is a very light full text search engine.
|
||||
|
||||
# meilisearch:
|
||||
# container_name: meilisearch
|
||||
# image: getmeili/meilisearch:v1.1.1
|
||||
# environment:
|
||||
# - MEILI_ENV=${MEILI_ENV:-development}
|
||||
# ports:
|
||||
# - "7700:7700"
|
||||
# networks:
|
||||
# - calcnet
|
||||
# volumes:
|
||||
# - ./meili_data:/meili_data
|
||||
# restart: unless-stopped
|
||||
|
||||
# sonic:
|
||||
# restart: unless-stopped
|
||||
# image: docker.io/valeriansaliou/sonic:v1.4.0
|
||||
# logging:
|
||||
# driver: none
|
||||
# networks:
|
||||
# - calcnet
|
||||
# volumes:
|
||||
# - ./sonic:/var/lib/sonic/store
|
||||
# - ./sonic/config.cfg:/etc/sonic.cfg
|
||||
|
||||
networks:
|
||||
calcnet:
|
||||
# web:
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
Breaking changes are indicated by the :warning: icon.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- `admin/search/index-all` is removed since posts are now indexed automatically.
|
||||
|
||||
## v20240301
|
||||
|
||||
- With the addition of new features, the following endpoints are added:
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
Critical security updates are indicated by the :warning: icon.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Introduce new full-text search engine
|
||||
|
||||
## v20240301
|
||||
|
||||
- Add a page (`/my/follow-requests/sent`) to check your follow requests that haven't been approved
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'Pgroonga1698420787202',
|
||||
'ChangeDefaultConfigs1709251460718',
|
||||
'AddReplyMuting1704851359889',
|
||||
'FixNoteUrlIndex1709129810501',
|
||||
|
@ -12,6 +13,12 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'RemoveNativeUtilsMigration1705877093218'
|
||||
);
|
||||
|
||||
-- pgroonga
|
||||
DROP INDEX "IDX_f27f5d88941e57442be75ba9c8";
|
||||
DROP INDEX "IDX_065d4d8f3b5adb4a08841eae3c";
|
||||
DROP INDEX "IDX_fcb770976ff8240af5799e3ffc";
|
||||
DROP EXTENSION pgroonga CASCADE;
|
||||
|
||||
-- change-default-configs
|
||||
ALTER TABLE "user_profile" ALTER COLUMN "noCrawle" SET DEFAULT false;
|
||||
ALTER TABLE "user_profile" ALTER COLUMN "publicReactions" SET DEFAULT false;
|
||||
|
|
|
@ -1,3 +1,97 @@
|
|||
# Unreleased
|
||||
|
||||
The full-text search engine in Firefish has been changed to [PGroonga](https://pgroonga.github.io/). This is no longer an optional feature, so please enable PGroonga on your system. If you are using Sonic, Meilisearch, or Elasticsearch, you can also uninstall it from your system and remove the settings from `.config/default.yml`.
|
||||
|
||||
## For systemd/pm2 users
|
||||
|
||||
### 1. Install PGroonga
|
||||
|
||||
Please execute `psql --version` to check your PostgreSQL major version. This will print a message like this:
|
||||
|
||||
```text
|
||||
psql (PostgreSQL) 16.1
|
||||
```
|
||||
|
||||
In this case, your PostgreSQL major version is `16`.
|
||||
|
||||
There are official installation instructions for many operating systems on <https://pgroonga.github.io/install>, so please follow the instructions on this page. However, since many users are using Ubuntu, and there are no instructions for Arch Linux, we explicitly list the instructions for Ubuntu and Arch Linux here. Please keep in mind that this is not official information and the procedures may change.
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
1. Add apt repository
|
||||
```sh
|
||||
sudo apt install -y software-properties-common
|
||||
sudo add-apt-repository -y universe
|
||||
sudo add-apt-repository -y ppa:groonga/ppa
|
||||
sudo apt install -y wget lsb-release
|
||||
wget https://packages.groonga.org/ubuntu/groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
sudo apt install -y -V ./groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release --codename --short)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
```
|
||||
2. Install PGroonga
|
||||
```sh
|
||||
# Please replace "16" with your PostgreSQL major version
|
||||
sudo apt install postgresql-16-pgdg-pgroonga
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
You can install PGroonga from the Arch User Repository.
|
||||
|
||||
```sh
|
||||
git clone https://aur.archlinux.org/pgroonga.git && cd pgroonga && makepkg -si
|
||||
# or paru -S pgroonga
|
||||
# or yay -S pgroonga
|
||||
```
|
||||
|
||||
### 2. Enable PGroonga
|
||||
|
||||
After the instllation, please execute this command to enable PGroonga:
|
||||
|
||||
```sh
|
||||
sudo --user=postgres psql --dbname=your_database_name --command='CREATE EXTENSION pgroonga;'
|
||||
```
|
||||
|
||||
The database name can be found in `.config/default.yml`.
|
||||
```yaml
|
||||
db:
|
||||
port: 5432
|
||||
db: database_name # substitute your_database_name with this
|
||||
user: firefish
|
||||
pass: password
|
||||
```
|
||||
|
||||
## For Docker/Podman users
|
||||
|
||||
Please edit your `docker-compose.yml` to replace the database container image from `docker.io/postgres` to `docker.io/groonga/pgroonga`.
|
||||
|
||||
Please make sure to use the same PostgreSQL version. If you are using `docker.io/postgres:16-alpine` (PostgreSQL v16), the corresponding image tag is `docker.io/groonga/pgroonga:latest-alpine-16` (or `docker.io/groonga/pgroonga:latest-alpine-16-slim`).
|
||||
|
||||
The list of tags can be found on <https://hub.docker.com/r/groonga/pgroonga/tags>.
|
||||
|
||||
```yaml
|
||||
db:
|
||||
restart: unless-stopped
|
||||
image: docker.io/groonga/pgroonga:latest-alpine-16-slim # change here
|
||||
container_name: firefish_db
|
||||
```
|
||||
|
||||
After that, execute this command to enable PGroonga:
|
||||
|
||||
```sh
|
||||
docker-compose up db --detach && docker-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
# or podman-compose up db --detach && podman-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
```
|
||||
|
||||
Once this is done, you can start Firefish as usual.
|
||||
|
||||
```sh
|
||||
docker pull registry.firefish.dev/firefish/firefish && docker-compose up --detach
|
||||
# or podman pull registry.firefish.dev/firefish/firefish && podman-compose up --detach
|
||||
```
|
||||
|
||||
# v20240301
|
||||
|
||||
## For all users
|
||||
|
|
|
@ -1831,11 +1831,6 @@ pushNotificationAlreadySubscribed: Les notificacions push ja estan activades
|
|||
pushNotificationNotSupported: El vostre navegador o servidor no admet notificacions
|
||||
push
|
||||
license: Llicència
|
||||
indexPosts: Índex de publicacions
|
||||
indexFrom: Índex a partir de l'ID de Publicacions
|
||||
indexFromDescription: Deixeu en blanc per indexar cada publicació
|
||||
indexNotice: Ara indexant. Això probablement trigarà una estona, si us plau, no reinicieu
|
||||
el servidor durant almenys una hora.
|
||||
_instanceTicker:
|
||||
none: No mostrar mai
|
||||
remote: Mostra per a usuaris remots
|
||||
|
|
|
@ -1500,9 +1500,6 @@ _widgets:
|
|||
chooseList: Wählen Sie eine Liste aus
|
||||
userList: Benutzerliste
|
||||
serverInfo: Server-Infos
|
||||
meiliStatus: Server-Status
|
||||
meiliSize: Indexgröße
|
||||
meiliIndexCount: Indexierte Beiträge
|
||||
_cw:
|
||||
hide: "Verbergen"
|
||||
show: "Inhalt anzeigen"
|
||||
|
@ -2083,7 +2080,6 @@ preventAiLearning: KI gestütztes bot-scraping unterdrücken
|
|||
preventAiLearningDescription: Fordern Sie KI-Sprachmodelle von Drittanbietern auf,
|
||||
die von Ihnen hochgeladenen Inhalte, wie z. B. Beiträge und Bilder, nicht zu untersuchen.
|
||||
license: Lizenz
|
||||
indexPosts: Gelistete Beiträge
|
||||
migrationConfirm: "Sind Sie absolut sicher, dass Sie Ihr Nutzerkonto zu diesem {account}
|
||||
umziehen möchten? Sobald Sie dies bestätigt haben, kann dies nicht mehr rückgängig
|
||||
gemacht werden und Ihr Nutzerkonto kann nicht mehr von ihnen genutzt werden.\nStellen
|
||||
|
@ -2111,9 +2107,6 @@ _experiments:
|
|||
kann es zu Verlangsamungen beim Laden während des Imports kommen.
|
||||
noGraze: Bitte deaktivieren Sie die Browsererweiterung "Graze for Mastodon", da sie
|
||||
die Funktion von Firefish stört.
|
||||
indexFrom: Indexieren ab Beitragskennung aufwärts
|
||||
indexNotice: Wird jetzt indexiert. Dies wird wahrscheinlich eine Weile dauern, bitte
|
||||
starten Sie Ihren Server für mindestens eine Stunde nicht neu.
|
||||
customKaTeXMacroDescription: "Richten Sie Makros ein, um mathematische Ausdrücke einfach
|
||||
zu schreiben! Die Notation entspricht den LaTeX-Befehlsdefinitionen und wird als
|
||||
\\newcommand{\\name}{content} oder \\newcommand{\\name}[number of arguments]{content}
|
||||
|
@ -2132,7 +2125,6 @@ expandOnNoteClick: Beitrag bei Klick öffnen
|
|||
image: Bild
|
||||
video: Video
|
||||
audio: Audio
|
||||
indexFromDescription: Leer lassen, um jeden Beitrag zu indexieren
|
||||
_filters:
|
||||
fromUser: Von Benutzer
|
||||
notesAfter: Beiträge nach
|
||||
|
|
|
@ -1092,11 +1092,6 @@ migrationConfirm: "Are you absolutely sure you want to migrate your account to {
|
|||
as the account you're moving from."
|
||||
defaultReaction: "Default emoji reaction for outgoing and incoming posts"
|
||||
license: "License"
|
||||
indexPosts: "Index Posts"
|
||||
indexFrom: "Index from Post ID onwards"
|
||||
indexFromDescription: "Leave blank to index every post"
|
||||
indexNotice: "Now indexing. This will probably take a while, please don't restart
|
||||
your server for at least an hour."
|
||||
customKaTeXMacro: "Custom KaTeX macros"
|
||||
customKaTeXMacroDescription: "Set up macros to write mathematical expressions easily!
|
||||
The notation conforms to the LaTeX command definitions and is written as \\newcommand{\\
|
||||
|
@ -1690,9 +1685,6 @@ _widgets:
|
|||
serverInfo: "Server Info"
|
||||
_userList:
|
||||
chooseList: "Select a list"
|
||||
meiliStatus: "Server Status"
|
||||
meiliSize: "Index size"
|
||||
meiliIndexCount: "Indexed posts"
|
||||
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
|
|
|
@ -1474,9 +1474,6 @@ _widgets:
|
|||
_userList:
|
||||
chooseList: Seleccione una lista
|
||||
serverInfo: Información del servidor
|
||||
meiliStatus: Estado del servidor
|
||||
meiliSize: Tamaño del índice
|
||||
meiliIndexCount: Publicaciones indizadas
|
||||
_cw:
|
||||
hide: "Ocultar"
|
||||
show: "Ver más"
|
||||
|
@ -2123,13 +2120,10 @@ moveFromDescription: 'Esto pondrá un alias en tu cuenta antigua para así poder
|
|||
ingresa la etiqueta de la cuenta con el formato siguiente: @persona@servidor.tld'
|
||||
defaultReaction: Emoji por defecto para reaccionar a las publicaciones entrantes y
|
||||
salientes
|
||||
indexFromDescription: Deja en blanco para indizar todas las publicaciones
|
||||
deletePasskeys: Borrar claves de paso
|
||||
deletePasskeysConfirm: Esto borrará irreversiblemente todas las claves de paso y de
|
||||
seguridad en esta cuenta, ¿Proceder?
|
||||
inputNotMatch: Las entradas no coinciden
|
||||
indexFrom: Indizar desde la ID de la publicación en adelante
|
||||
indexPosts: Indizar publicaciones
|
||||
isModerator: Moderador
|
||||
isAdmin: Administrador
|
||||
isPatron: Mecenas de Firefish
|
||||
|
@ -2139,8 +2133,6 @@ migrationConfirm: "¿Estás absolutamente seguro de que quieres migrar a tu cuen
|
|||
{account}? Una vez hecho esto, no podrás revertir el cambio, ni tampoco usar tu
|
||||
cuenta normalmente.\nTambién, asegúrate de que has configurado ésta cuenta como
|
||||
la cuenta desde la cual estás migrando."
|
||||
indexNotice: Indizando ahora. Esto puede llevar bastante tiempo, por favor, no reinicies
|
||||
el servidor por lo menos hasta dentro de una hora.
|
||||
customKaTeXMacro: Macros KaTeX personalizadas
|
||||
customKaTeXMacroDescription: '¡Configura macros para escribir expresiones matemáticas
|
||||
fácilmente! La notación es conforme la las definiciones de comandos LaTeX y puede
|
||||
|
|
|
@ -1400,10 +1400,7 @@ _widgets:
|
|||
_userList:
|
||||
chooseList: Sélectionner une liste
|
||||
unixClock: Horloge UNIX
|
||||
meiliIndexCount: Publications indexées
|
||||
serverInfo: Info serveur
|
||||
meiliStatus: État du serveur
|
||||
meiliSize: Taille de l'index
|
||||
instanceCloud: Nuage de serveurs
|
||||
rssTicker: Bandeau RSS
|
||||
_cw:
|
||||
|
@ -2061,9 +2058,6 @@ moveToLabel: 'Compte vers lequel vous migrez :'
|
|||
moveFrom: Migrer vers ce compte depuis un ancien compte
|
||||
defaultReaction: Émoji de réaction par défaut pour les publications entrantes et sortantes
|
||||
license: Licence
|
||||
indexPosts: Indexer les publications
|
||||
indexNotice: Indexation en cours. Cela prendra certainement du temps, veuillez ne
|
||||
pas redémarrer votre serveur pour au moins une heure.
|
||||
customKaTeXMacro: Macros KaTeX personnalisées
|
||||
enableCustomKaTeXMacro: Activer les macros KaTeX personnalisées
|
||||
noteId: ID des publications
|
||||
|
@ -2110,7 +2104,6 @@ expandOnNoteClick: Ouvrir la publications en cliquant
|
|||
preventAiLearning: Empêcher le récupération de données par des IA
|
||||
listsDesc: Les listes vous laissent créer des fils personnalisés avec des utilisateur·rice·s
|
||||
spécifié·e·s. Elles sont accessibles depuis la page des fils.
|
||||
indexFromDescription: Laisser vide pour indexer toutes les publications
|
||||
_feeds:
|
||||
jsonFeed: flux JSON
|
||||
atom: Atom
|
||||
|
@ -2120,7 +2113,6 @@ alt: ALT
|
|||
swipeOnMobile: Permettre le balayage entre les pages
|
||||
expandOnNoteClickDesc: Si désactivé, vous pourrez toujours ouvrir les publications
|
||||
dans le menu du clic droit et en cliquant sur l'horodatage.
|
||||
indexFrom: Indexer à partir de l'ID des publications
|
||||
older: ancien
|
||||
newer: récent
|
||||
accessibility: Accessibilité
|
||||
|
|
|
@ -1390,14 +1390,11 @@ _widgets:
|
|||
aiscript: "Konsol AiScript"
|
||||
aichan: "Ai"
|
||||
rssTicker: Telegraf RSS
|
||||
meiliIndexCount: Postingan yang terindeks
|
||||
userList: Daftar Pengguna
|
||||
instanceCloud: Server Awan
|
||||
unixClock: Jam UNIX
|
||||
meiliSize: Ukuran indeks
|
||||
_userList:
|
||||
chooseList: Pilih daftar
|
||||
meiliStatus: Status Server
|
||||
serverInfo: Info Server
|
||||
_cw:
|
||||
hide: "Sembunyikan"
|
||||
|
@ -1984,8 +1981,6 @@ speed: Kecepatan
|
|||
slow: Pelan
|
||||
remoteOnly: Jarak jauh saja
|
||||
moveFrom: Dari akun lama pindahkan ke akun ini
|
||||
indexNotice: Sedang mengindeks. Ini memerlukan beberapa waktu, mohon jangan mulai
|
||||
ulang server setidaknya satu jam.
|
||||
sendPushNotificationReadMessage: Hapus pemberitahuan dorong saat pemberitahuan atau
|
||||
pesan relevan sudah dibaca
|
||||
moveAccountDescription: Proses ini permanen. Pastikan kamu sudah mengatur alias dari
|
||||
|
@ -2010,7 +2005,6 @@ showAds: Tampilkan spanduk komunitas
|
|||
enterSendsMessage: Tekan Enter pada Pesan untuk mengirim pesan (matikan dengan Ctrl
|
||||
+ Enter)
|
||||
showAdminUpdates: Indikasi versi Firefish baru tersedia (hanya admin)
|
||||
indexFrom: Indeks dari Post ID berikutnya
|
||||
noteId: ID Postingan
|
||||
findOtherInstance: Cari server lain
|
||||
caption: Deskripsi itomatis
|
||||
|
@ -2022,7 +2016,6 @@ moveFromDescription: Ini akan mengatur alias akun lamamu jadi kamu dapat pindah
|
|||
akun tersebut ke akun sekarang. Lakukan ini SEBELUM memindahkan akun lama. Silakan
|
||||
masukkan tag akun dengan format seperti @orang@server.com
|
||||
defaultReaction: Reaksi emoji bawaan untuk postingan keluar dan masuk
|
||||
indexPosts: Indeks Postingan
|
||||
preventAiLearning: Cegah scraping bot AI
|
||||
customKaTeXMacro: Makro KaTeX khusus
|
||||
sendPushNotificationReadMessageCaption: Pemberitahuan yang berisi teks "{emptyPushNotificationMessage}"
|
||||
|
@ -2052,7 +2045,6 @@ migrationConfirm: "Kamu sangat yakin ingin memindahkan akunmu ke {account}? Seka
|
|||
lagi secara normal. \nDan juga, harap pastikan kamu sudah mengatur akun sekarang
|
||||
sebagai akun yang dipindahkan."
|
||||
license: Lisensi
|
||||
indexFromDescription: Kosongkan untuk mengindeks setiap postingan
|
||||
noGraze: Harap nonaktifkan ekstensi peramban "Graze for Mastodon", karena akan menganggu
|
||||
Firefish.
|
||||
silencedWarning: Halaman ini tampil karena pengguna ini datang dari server yang dibisukan
|
||||
|
|
|
@ -1324,9 +1324,6 @@ _widgets:
|
|||
instanceCloud: Cloud del server
|
||||
unixClock: Orologio UNIX
|
||||
serverInfo: Informazioni sul server
|
||||
meiliIndexCount: Post indicizzati
|
||||
meiliStatus: Stato del server
|
||||
meiliSize: Dimensione indice
|
||||
userList: Elenco utenti
|
||||
_cw:
|
||||
hide: "Nascondi"
|
||||
|
@ -1838,9 +1835,7 @@ customSplashIconsDescription: Elenco degli URL di icone personalizzate da mostra
|
|||
le immagini siano su un URL statico, preferibilmente di dimensioni 192x192.
|
||||
swipeOnDesktop: Permetti lo swipe su desktop simile alla versione mobile
|
||||
logoImageUrl: URL del logo
|
||||
indexFrom: Indicizza dal post ID
|
||||
customKaTeXMacro: Macro KaTeX personalizzate
|
||||
indexPosts: Crea indice dei post
|
||||
signupsDisabled: Le iscrizioni su questo server al momento non sono possibili, ma
|
||||
puoi sempre iscriverti su un altro server! Se invece hai un codice di invito per
|
||||
questo server, inseriscilo qua sotto.
|
||||
|
@ -1919,9 +1914,6 @@ lastActiveDate: Ultimo utilizzo
|
|||
enterSendsMessage: Premi "Invio" nei messaggi per inviare (altrimenti è "Ctrl + Invio")
|
||||
customMOTD: Messaggi di caricamento personalizzati (splash screen)
|
||||
replayTutorial: Ripeti il tutorial
|
||||
indexFromDescription: Lascia vuoto per indicizzare tutti i post
|
||||
indexNotice: Creazione indice in corso. Sarà necessario del tempo, fai attenzione
|
||||
a non riavviare il server per almeno un'ora.
|
||||
enableCustomKaTeXMacro: Abilita le macro KaTeX personalizzate
|
||||
preventAiLearningDescription: Richiedi ai bot di intelligenza artificiale di terze
|
||||
parti di non studiare e acquisire il contenuto che carichi, come post e immagini.
|
||||
|
|
|
@ -974,10 +974,6 @@ migrationConfirm: "本当にこのアカウントを {account} に引っ越し
|
|||
この操作を行う前に引っ越し先のアカウントでエイリアスを作成する必要があります。エイリアスが作成されているか、必ず確認してください。"
|
||||
defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション"
|
||||
license: "ライセンス"
|
||||
indexPosts: "投稿をインデックス"
|
||||
indexFrom: "この投稿ID以降をインデックスする"
|
||||
indexFromDescription: "空白で全ての投稿を指定します"
|
||||
indexNotice: "インデックスを開始しました。完了まで時間がかかる場合があるため、少なくとも1時間はサーバーを再起動しないでください。"
|
||||
customKaTeXMacro: "カスタムKaTeXマクロ"
|
||||
customKaTeXMacroDescription: "数式入力を楽にするためのマクロを設定しましょう!記法はLaTeXにおけるコマンドの定義と同様に \\newcommand{\\
|
||||
name}{content} または \\newcommand{\\add}[2]{#1 + #2} のように記述します。後者の例では \\add{3}{foo}
|
||||
|
@ -1453,10 +1449,7 @@ _widgets:
|
|||
userList: "ユーザーリスト"
|
||||
_userList:
|
||||
chooseList: "リストを選択"
|
||||
meiliStatus: サーバーステータス
|
||||
serverInfo: サーバー情報
|
||||
meiliSize: インデックスサイズ
|
||||
meiliIndexCount: インデックス済みの投稿
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
|
|
|
@ -1331,10 +1331,7 @@ _widgets:
|
|||
serverInfo: 서버 정보
|
||||
_userList:
|
||||
chooseList: 리스트 선택
|
||||
meiliStatus: 서버 정보
|
||||
userList: 유저 목록
|
||||
meiliSize: 인덱스 크기
|
||||
meiliIndexCount: 인덱싱 완료된 게시물
|
||||
rssTicker: RSS Ticker
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
|
@ -1840,7 +1837,6 @@ customSplashIconsDescription: 유저가 페이지를 로딩/새로고침할 때
|
|||
이미지는 되도록 정적 URL으로 구성하고, 192x192 해상도로 조정하여 주십시오.
|
||||
moveFromDescription: '이전 계정에 대한 별칭을 작성하여, 이 계정으로 옮길 수 있도록 합니다. 반드시 계정을 이전하기 전에 수행해야
|
||||
합니다. 이전 계정을 다음과 같은 형식으로 입력하여 주십시오: @person@server.com'
|
||||
indexFromDescription: 빈 칸으로 두면 모든 게시물을 인덱싱합니다
|
||||
customKaTeXMacroDescription: 'KaTeX 매크로를 지정하여 수식을 더욱 편리하게 입력하세요! LaTeX의 커맨드 정의와 동일하게
|
||||
\newcommand{\ 이름}{내용} 또는 \newcommand{\이름}[인수 갯수]{내용} 와 같이 입력하십시오. 예를 들어 \newcommand{\add}[2]{#1
|
||||
+ #2} 와 같이 정의한 경우 \add{3}{foo} 를 입력하면 3 + foo 으로 치환됩니다.매크로의 이름을 감싸는 중괄호를 소괄호() 또는
|
||||
|
@ -1895,7 +1891,6 @@ accessibility: 접근성
|
|||
userSaysSomethingReasonReply: '{name} 님이 {reason} 을 포함하는 게시물에 답글했습니다'
|
||||
userSaysSomethingReasonRenote: '{name} 님이 {reason} 을 포함하는 게시물을 부스트했습니다'
|
||||
breakFollowConfirm: 팔로워를 해제하시겠습니까?
|
||||
indexFrom: 이 게시물 ID부터 인덱싱하기
|
||||
noThankYou: 괜찮습니다
|
||||
hiddenTags: 숨길 해시태그
|
||||
image: 이미지
|
||||
|
@ -1927,8 +1922,6 @@ removeMember: 멤버를 삭제
|
|||
license: 라이선스
|
||||
migrationConfirm: "정말로 이 계정을 {account}로 이사하시겠습니까? 한 번 이사하면, 현재 이 계정은 두 번 다시 사용할 수
|
||||
없게 됩니다.\n또한, 이사 갈 계정에 현재 사용 중인 계정의 별칭을 올바르게 작성하였는지 다시 한 번 확인하십시오."
|
||||
indexPosts: 게시물을 인덱싱
|
||||
indexNotice: 인덱싱을 시작했습니다. 이 작업은 시간이 많이 소요되므로, 최소 1시간 이내에 서버를 재시작하지 마십시오.
|
||||
noteId: 게시물 ID
|
||||
signupsDisabled: 현재 이 서버에서는 신규 등록을 받고 있지 않습니다. 초대 코드를 가지고 계신 경우 아래 칸에 입력해 주십시오. 초대
|
||||
코드를 가지고 있지 않더라도, 신규 등록이 열려 있는 다른 서버에 등록하실 수 있습니다!
|
||||
|
|
|
@ -939,7 +939,6 @@ allowedInstancesDescription: Tjenernavn for tjenere som skal hvitelistes. En per
|
|||
(Vil bare bli brukt i privat modus).
|
||||
previewNoteText: Forhåndsvisning
|
||||
recentNDays: Siste {n} dager
|
||||
indexPosts: Indekser poster
|
||||
objectStorageUseProxy: Koble til gjennom en mellomtjener
|
||||
objectStorageUseProxyDesc: Skru av dette dersom du ikke vil bruke mellomtjenere for
|
||||
API-oppkoblinger
|
||||
|
@ -1185,10 +1184,6 @@ moveFromDescription: Dette vil sette opp et alias for din gamle kontoen slik at
|
|||
kan flytte fra den gamle kontoen til denne. Gjør dette FØR du flytter fra den gamle
|
||||
kontoen. Skriv inn den gamle kontoen på formen @person@server.com
|
||||
defaultReaction: Standard emoji-reaksjon for utgående og innkommende poster
|
||||
indexFrom: Indekser poster fra post-id og fremover
|
||||
indexNotice: Indekserer. Dette vil sannsynligvis ta litt tid, ikke restart tjeneren
|
||||
før det har gått minst en time.
|
||||
indexFromDescription: La stå tom for å indeksere alle poster
|
||||
customKaTeXMacroDescription: 'Sett opp makroer for å skrive matematiske uttrykk enkelt.
|
||||
Notasjonen følger LaTeX-kommandoer og er skrevet som \newcommand{\ navn}{uttrykk}
|
||||
eller \newcommand{\navn}{antall argumenter}{uttrykk}. For eksempel vil \newcommand{\add}{2}{#1
|
||||
|
@ -1628,14 +1623,12 @@ _antennaSources:
|
|||
instances: Poster fra alle brukerne på denne tjeneren
|
||||
_widgets:
|
||||
timeline: Tidslinje
|
||||
meiliSize: Indeks-størrelse
|
||||
instanceCloud: Tjenersky
|
||||
onlineUsers: Påloggede brukere
|
||||
clock: Klokke
|
||||
userList: Brukerliste
|
||||
rss: RSS-leser
|
||||
serverMetric: Tjenermetrikker
|
||||
meiliIndexCount: Indekserte poster
|
||||
button: Knapp
|
||||
unixClock: Unix-klokke
|
||||
calendar: Kalender
|
||||
|
@ -1647,7 +1640,6 @@ _widgets:
|
|||
photos: Bilder
|
||||
rssTicker: RSS-rulletekst
|
||||
aiscript: AiScript-konsoll
|
||||
meiliStatus: Tjenerstatus
|
||||
memo: Notatlapp
|
||||
notifications: Varsler
|
||||
postForm: Ny post
|
||||
|
|
|
@ -1892,11 +1892,6 @@ sendPushNotificationReadMessageCaption: Powiadomienie zawierające tekst "{empty
|
|||
baterii Twojego urządzenia.
|
||||
defaultReaction: Domyślna reakcja emoji dla wychodzących i przychodzących wpisów
|
||||
license: Licencja
|
||||
indexPosts: Indeksuj wpisy
|
||||
indexFrom: Indeksuj wpisy od ID
|
||||
indexFromDescription: Zostaw puste dla indeksowania wszystkich wpisów
|
||||
indexNotice: Indeksuję. Zapewne zajmie to chwilę, nie restartuj serwera przez co najmniej
|
||||
godzinę.
|
||||
customKaTeXMacro: Niestandardowe makra KaTeX
|
||||
enableCustomKaTeXMacro: Włącz niestandardowe makra KaTeX
|
||||
noteId: ID wpisu
|
||||
|
|
|
@ -1375,9 +1375,6 @@ _widgets:
|
|||
userList: Список пользователей
|
||||
_userList:
|
||||
chooseList: Выберите список
|
||||
meiliStatus: Состояние сервера
|
||||
meiliSize: Размер индекса
|
||||
meiliIndexCount: Индексированные посты
|
||||
serverInfo: Информация о сервере
|
||||
_cw:
|
||||
hide: "Спрятать"
|
||||
|
@ -1951,11 +1948,6 @@ showUpdates: Показывать всплывающее окно при обн
|
|||
recommendedInstances: Рекомендованные серверы
|
||||
defaultReaction: Эмодзи реакция по умолчанию для выходящих и исходящих постов
|
||||
license: Лицензия
|
||||
indexPosts: Индексировать посты
|
||||
indexFrom: Индексировать начиная с идентификатора поста и далее
|
||||
indexFromDescription: оставьте пустым для индексации каждого поста
|
||||
indexNotice: Теперь индексирование. Вероятно, это займет некоторое время, пожалуйста,
|
||||
не перезагружайте свой сервер по крайней мере в течение часа.
|
||||
customKaTeXMacro: Кастомные KaTex макросы
|
||||
enableCustomKaTeXMacro: Включить кастомные KaTeX макросы
|
||||
noteId: Идентификатор поста
|
||||
|
|
|
@ -1316,8 +1316,6 @@ customMOTDDescription: ข้อความหน้าจอเริ่มต
|
|||
คั่นด้วยการขึ้นบรรทัดใหม่เพื่อแสดงแบบสุ่มทุกครั้งที่ผู้ใช้โหลดเว็บหรือโหลดหน้าเว็บซ้ำ
|
||||
caption: คำอธิบายโดยอัตโนมัติ
|
||||
moveToLabel: 'บัญชีที่คุณจะย้ายไปยัง:'
|
||||
indexFromDescription: เว้นว่างไว้เพื่อสร้างดัชนีทุกโพสต์
|
||||
indexNotice: ตอนนี้กำลังจัดทำดัชนี การดำเนินการนี้อาจใช้เวลาสักครู่ โปรดอย่ารีสตาร์ทเซิร์ฟเวอร์เป็นเวลาอย่างน้อยหนึ่งชั่วโมง
|
||||
noteId: โพสต์ ID
|
||||
apps: แอป
|
||||
enableRecommendedTimeline: เปิดใช้งาน ไทม์ไลน์ที่แนะนำ
|
||||
|
@ -1371,9 +1369,7 @@ moveFromDescription: การดำเนินการนี้จะตั
|
|||
migrationConfirm: "คุณแน่ใจหรือไม่ว่าคุณต้องการย้ายบัญชีของคุณไปยัง {account} เมื่อคุณทำเช่นนี้
|
||||
คุณจะไม่สามารถกู้คืนมาได้ และคุณจะไม่สามารถใช้บัญชีของคุณได้ตามปกติอีก\nนอกจากนี้
|
||||
โปรดตรวจสอบให้แน่ใจว่าคุณได้ตั้งบัญชีปัจจุบันนี้เป็นบัญชีที่คุณจะย้ายออก"
|
||||
indexFrom: จัดทำดัชนีตั้งแต่ Post ID เป็นต้นไป
|
||||
license: ใบอนุญาต
|
||||
indexPosts: ดัชนีโพสต์
|
||||
signupsDisabled: การลงชื่อสมัครใช้บนเซิร์ฟเวอร์นี้ถูกปิดใช้งานอยู่ในขณะนี้ แต่คุณสามารถสมัครที่เซิร์ฟเวอร์อื่นได้ตลอดเวลา
|
||||
หากคุณมีรหัสเชิญสำหรับเซิร์ฟเวอร์นี้ โปรดป้อนรหัสด้านล่าง
|
||||
customKaTeXMacroDescription: 'ตั้งค่ามาโครเพื่อเขียนนิพจน์ทางคณิตศาสตร์ได้อย่างง่ายดาย
|
||||
|
|
|
@ -158,7 +158,6 @@ _widgets:
|
|||
activity: Aktivite
|
||||
digitalClock: Dijital Saat
|
||||
unixClock: UNIX Saati
|
||||
meiliIndexCount: Indexlenmiş gönderiler
|
||||
calendar: Takvim
|
||||
trends: Popüler
|
||||
memo: Yapışkan Notlar
|
||||
|
@ -166,13 +165,11 @@ _widgets:
|
|||
federation: Federasyon
|
||||
instanceCloud: Sunucu Bulutu
|
||||
postForm: Gönderi Formu
|
||||
meiliSize: Index boyutu
|
||||
slideshow: Slayt Gösterisi
|
||||
button: Düğme
|
||||
clock: Saat
|
||||
rss: RSS Okuyucu
|
||||
serverInfo: Sunucu Bilgisi
|
||||
meiliStatus: Sunucu Durumu
|
||||
jobQueue: İş Sırası
|
||||
serverMetric: Sunucu Bilgileri
|
||||
_profile:
|
||||
|
@ -473,7 +470,6 @@ activeEmailValidationDescription: Tek kullanımlık adreslerin kontrol edilmesi
|
|||
sağlar. İşaretlenmediğinde, yalnızca e-postanın biçimi doğrulanır.
|
||||
move: Taşı
|
||||
defaultReaction: Giden ve gelen gönderiler için varsayılan emoji tepkisi
|
||||
indexPosts: Dizin Gönderileri
|
||||
youGotNewFollower: takip etti
|
||||
receiveFollowRequest: Takip isteği alındı
|
||||
followRequestAccepted: Takip isteği onaylandı
|
||||
|
@ -1084,7 +1080,6 @@ check: Kontrol Et
|
|||
driveCapOverrideLabel: Bu kullanıcı için drive kapasitesini değiştirin
|
||||
numberOfPageCache: Önbelleğe alınan sayfa sayısı
|
||||
license: Lisans
|
||||
indexFrom: Post ID'den itibaren dizin
|
||||
xl: XL
|
||||
notificationSetting: Bildirim ayarları
|
||||
fillAbuseReportDescription: Lütfen bu raporla ilgili ayrıntıları doldurun. Belirli
|
||||
|
@ -1159,9 +1154,6 @@ migrationConfirm: "Hesabınızı {account} hesabına taşımak istediğinizden k
|
|||
emin misiniz? Bunu yaptığınızda, geri alamazsınız ve hesabınızı bir daha normal
|
||||
şekilde kullanamazsınız.\nAyrıca, lütfen bu cari hesabı, taşındığınız hesap olarak
|
||||
ayarladığınızdan emin olun."
|
||||
indexFromDescription: Her gönderiyi dizine eklemek için boş bırakın
|
||||
indexNotice: Şimdi indeksleniyor. Bu muhtemelen biraz zaman alacaktır, lütfen sunucunuzu
|
||||
en az bir saat yeniden başlatmayın.
|
||||
customKaTeXMacro: Özel KaTeX makroları
|
||||
directNotes: Özel Mesajlar
|
||||
import: İçeri Aktar
|
||||
|
|
|
@ -1199,14 +1199,11 @@ _widgets:
|
|||
aiscript: "Консоль AiScript"
|
||||
_userList:
|
||||
chooseList: Оберіть список
|
||||
meiliStatus: Стан сервера
|
||||
meiliSize: Розмір індексу
|
||||
rssTicker: RSS-тікер
|
||||
instanceCloud: Хмара серверів
|
||||
unixClock: Годинник UNIX
|
||||
userList: Список користувачів
|
||||
serverInfo: Інформація про сервер
|
||||
meiliIndexCount: Індексовані записи
|
||||
_cw:
|
||||
hide: "Сховати"
|
||||
show: "Показати більше"
|
||||
|
@ -1977,11 +1974,6 @@ caption: Автоматичний опис
|
|||
showAdminUpdates: Вказати, що доступна нова версія Firefish (тільки для адміністратора)
|
||||
defaultReaction: Емодзі реакція за замовчуванням для вихідних і вхідних записів
|
||||
license: Ліцензія
|
||||
indexPosts: Індексувати пости
|
||||
indexFrom: Індексувати записи з ID
|
||||
indexFromDescription: Залиште порожнім, щоб індексувати кожен запис
|
||||
indexNotice: Зараз відбувається індексація. Це, ймовірно, займе деякий час, будь ласка,
|
||||
не перезавантажуйте сервер принаймні годину.
|
||||
signupsDisabled: Реєстрація на цьому сервері наразі відключена, але ви завжди можете
|
||||
зареєструватися на іншому сервері! Якщо у вас є код запрошення на цей сервер, будь
|
||||
ласка, введіть його нижче.
|
||||
|
|
|
@ -1437,9 +1437,6 @@ _widgets:
|
|||
userList: Danh sách người dùng
|
||||
_userList:
|
||||
chooseList: Chọn một danh sách
|
||||
meiliSize: Kích cỡ chỉ mục
|
||||
meiliIndexCount: Tút đã lập chỉ mục
|
||||
meiliStatus: Trạng thái máy chủ
|
||||
serverInfo: Thông tin máy chủ
|
||||
_cw:
|
||||
hide: "Ẩn"
|
||||
|
@ -2048,8 +2045,6 @@ hiddenTagsDescription: 'Liệt kê các hashtag (không có #) mà bạn muốn
|
|||
noInstances: Không có máy chủ nào
|
||||
manageGroups: Quản lý nhóm
|
||||
accessibility: Khả năng tiếp cận
|
||||
indexNotice: Đang lập chỉ mục. Quá trình này có thể mất một lúc, vui lòng không khởi
|
||||
động lại máy chủ của bạn sau ít nhất một giờ.
|
||||
breakFollowConfirm: Bạn có chắc muốn xóa người theo dõi?
|
||||
caption: Caption tự động
|
||||
objectStorageS3ForcePathStyle: Sử dụng URL điểm cuối dựa trên đường dẫn
|
||||
|
@ -2069,7 +2064,6 @@ updateAvailable: Có bản cập nhật mới!
|
|||
swipeOnDesktop: Cho phép vuốt kiểu điện thoại trên máy tính
|
||||
moveFromLabel: 'Tài khoản cũ của bạn:'
|
||||
defaultReaction: Biểu cảm mặc định cho những tút đã đăng và sắp đăng
|
||||
indexFromDescription: Để trống để lập chỉ mục toàn bộ
|
||||
donationLink: Liên kết tới trang tài trợ
|
||||
deletePasskeys: Xóa passkey
|
||||
delete2faConfirm: Thao tác này sẽ xóa 2FA trên tài khoản này một cách không thể phục
|
||||
|
@ -2086,8 +2080,6 @@ audio: Âm thanh
|
|||
selectInstance: Chọn máy chủ
|
||||
userSaysSomethingReason: '{name} cho biết {reason}'
|
||||
pushNotification: Thông báo đẩy
|
||||
indexPosts: Chỉ mục tút
|
||||
indexFrom: Chỉ mục từ Post ID
|
||||
customKaTeXMacro: Tùy chỉnh macro KaTeX
|
||||
license: Giấy phép
|
||||
cw: Nội dung ẩn
|
||||
|
|
|
@ -1350,9 +1350,6 @@ _widgets:
|
|||
aiscript: "AiScript 控制台"
|
||||
aichan: "小蓝"
|
||||
userList: 用户列表
|
||||
meiliStatus: 服务器状态
|
||||
meiliIndexCount: 已索引的帖子
|
||||
meiliSize: 索引大小
|
||||
serverInfo: 服务器信息
|
||||
_userList:
|
||||
chooseList: 选择列表
|
||||
|
@ -1917,7 +1914,6 @@ isAdmin: 管理员
|
|||
findOtherInstance: 寻找其它服务器
|
||||
moveFromDescription: 这将为您的旧账号设置一个别名,以便您可以从该旧账号迁移到当前账号。在从旧账号迁移之前执行此操作。请输入格式如 @person@server.com
|
||||
的账号标签
|
||||
indexPosts: 索引帖子
|
||||
signupsDisabled: 该服务器目前关闭注册,但您随时可以在另一台服务器上注册!如果您有该服务器的邀请码,请在下面输入。
|
||||
silencedWarning: 显示这个页面是因为这些用户来自您的管理员设置的禁言服务器,所以他们有可能是垃圾信息。
|
||||
isBot: 这个账号是一个机器人
|
||||
|
@ -1931,12 +1927,9 @@ moveTo: 将当前账号迁移至新账号
|
|||
moveToLabel: 您要迁移到的目标账号:
|
||||
moveAccount: 迁移账号!
|
||||
migrationConfirm: "您确实确定要将账号迁移到 {account} 吗?此操作无法撤消,并且您将无法再次正常使用旧账号。\n另外,请确保您已将此当前账号设置为要移出的账号。"
|
||||
indexFromDescription: 留空以索引每个帖子
|
||||
noteId: 帖子 ID
|
||||
moveFrom: 从旧账号迁移至此账号
|
||||
defaultReaction: 发出和收到帖子的默认表情符号反应
|
||||
indexNotice: 现在开始索引。这可能需要一段时间,请至少一个小时内不要重新启动服务器。
|
||||
indexFrom: 从帖子 ID 开始的索引
|
||||
sendModMail: 发送审核通知
|
||||
isLocked: 该账号设置了关注请求
|
||||
_filters:
|
||||
|
|
|
@ -1349,9 +1349,6 @@ _widgets:
|
|||
userList: 使用者列表
|
||||
_userList:
|
||||
chooseList: 選擇一個清單
|
||||
meiliIndexCount: 編入索引的帖子
|
||||
meiliStatus: 伺服器狀態
|
||||
meiliSize: 索引大小
|
||||
_cw:
|
||||
hide: "隱藏"
|
||||
show: "瀏覽更多"
|
||||
|
@ -1879,8 +1876,6 @@ pushNotificationNotSupported: 你的瀏覽器或伺服器不支援推送通知
|
|||
accessibility: 輔助功能
|
||||
userSaysSomethingReasonReply: '{name} 回覆了包含 {reason} 的貼文'
|
||||
hiddenTags: 隱藏主題標籤
|
||||
indexPosts: 索引貼文
|
||||
indexNotice: 現在開始索引。 這可能需要一段時間,請不要在一個小時內重啟你的伺服器。
|
||||
deleted: 已刪除
|
||||
editNote: 編輯貼文
|
||||
edited: '於 {date} {time} 編輯'
|
||||
|
@ -1925,10 +1920,8 @@ sendModMail: 發送審核通知
|
|||
enableIdenticonGeneration: 啟用Identicon生成
|
||||
enableServerMachineStats: 啟用伺服器硬體統計資訊
|
||||
reactionPickerSkinTone: 首選表情符號膚色
|
||||
indexFromDescription: 留空以索引每個貼文
|
||||
preventAiLearning: 防止 AI 機器人抓取
|
||||
preventAiLearningDescription: 請求第三方 AI 語言模型不要研究您上傳的內容,例如貼文和圖像。
|
||||
indexFrom: 建立此貼文ID以後的索引
|
||||
isLocked: 該帳戶已獲得以下批准
|
||||
isModerator: 板主
|
||||
isAdmin: 管理員
|
||||
|
|
21
packages/backend/migration/1698420787202-pgroonga.js
Normal file
21
packages/backend/migration/1698420787202-pgroonga.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class Pgroonga1698420787202 {
|
||||
name = "Pgroonga1698420787202";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f27f5d88941e57442be75ba9c8" ON "note" USING "pgroonga" ("text")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_065d4d8f3b5adb4a08841eae3c" ON "user" USING "pgroonga" ("name" pgroonga_varchar_full_text_search_ops_v2)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_fcb770976ff8240af5799e3ffc" ON "user_profile" USING "pgroonga" ("description" pgroonga_varchar_full_text_search_ops_v2) `,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "IDX_fcb770976ff8240af5799e3ffc"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_065d4d8f3b5adb4a08841eae3c"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_f27f5d88941e57442be75ba9c8"`);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,6 @@
|
|||
"@bull-board/koa": "5.14.2",
|
||||
"@bull-board/ui": "5.14.2",
|
||||
"@discordapp/twemoji": "^15.0.2",
|
||||
"@elastic/elasticsearch": "8.12.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
|
@ -81,7 +80,6 @@
|
|||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"megalodon": "workspace:*",
|
||||
"meilisearch": "0.37.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"mime-types": "2.1.35",
|
||||
"msgpackr": "^1.10.1",
|
||||
|
@ -112,7 +110,6 @@
|
|||
"sanitize-html": "2.12.1",
|
||||
"semver": "7.6.0",
|
||||
"sharp": "0.33.2",
|
||||
"sonic-channel": "^1.3.1",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
|
|
|
@ -37,27 +37,6 @@ export type Source = {
|
|||
user?: string;
|
||||
tls?: { [z: string]: string };
|
||||
};
|
||||
elasticsearch: {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl?: boolean;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
index?: string;
|
||||
};
|
||||
sonic: {
|
||||
host: string;
|
||||
port: number;
|
||||
auth?: string;
|
||||
collection?: string;
|
||||
bucket?: string;
|
||||
};
|
||||
meilisearch: {
|
||||
host: string;
|
||||
port: number;
|
||||
apiKey?: string;
|
||||
ssl: boolean;
|
||||
};
|
||||
|
||||
proxy?: string;
|
||||
proxySmtp?: string;
|
||||
|
|
|
@ -2,7 +2,6 @@ import si from "systeminformation";
|
|||
import Xev from "xev";
|
||||
import * as osUtils from "os-utils";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
|
@ -30,7 +29,6 @@ export default function () {
|
|||
const memStats = await mem();
|
||||
const netStats = await net();
|
||||
const fsStats = await fs();
|
||||
const meilisearchStats = await meilisearchStatus();
|
||||
|
||||
const stats = {
|
||||
cpu: roundCpu(cpu),
|
||||
|
@ -47,7 +45,6 @@ export default function () {
|
|||
r: round(Math.max(0, fsStats.rIO_sec ?? 0)),
|
||||
w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
|
||||
},
|
||||
meilisearch: meilisearchStats,
|
||||
};
|
||||
ev.emit("serverStats", stats);
|
||||
log.unshift(stats);
|
||||
|
@ -86,16 +83,3 @@ async function fs() {
|
|||
const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
||||
return data || { rIO_sec: 0, wIO_sec: 0 };
|
||||
}
|
||||
|
||||
// MEILI STAT
|
||||
async function meilisearchStatus() {
|
||||
if (meilisearch) {
|
||||
return meilisearch.serverStats();
|
||||
} else {
|
||||
return {
|
||||
health: "unconfigured",
|
||||
size: 0,
|
||||
indexed_count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
import * as elasticsearch from "@elastic/elasticsearch";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
const index = {
|
||||
settings: {
|
||||
analysis: {
|
||||
analyzer: {
|
||||
ngram: {
|
||||
tokenizer: "ngram",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
properties: {
|
||||
text: {
|
||||
type: "text",
|
||||
index: true,
|
||||
analyzer: "ngram",
|
||||
},
|
||||
userId: {
|
||||
type: "keyword",
|
||||
index: true,
|
||||
},
|
||||
userHost: {
|
||||
type: "keyword",
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Init ElasticSearch connection
|
||||
const client = config.elasticsearch
|
||||
? new elasticsearch.Client({
|
||||
node: `${config.elasticsearch.ssl ? "https://" : "http://"}${
|
||||
config.elasticsearch.host
|
||||
}:${config.elasticsearch.port}`,
|
||||
auth:
|
||||
config.elasticsearch.user && config.elasticsearch.pass
|
||||
? {
|
||||
username: config.elasticsearch.user,
|
||||
password: config.elasticsearch.pass,
|
||||
}
|
||||
: undefined,
|
||||
pingTimeout: 30000,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (client) {
|
||||
client.indices
|
||||
.exists({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
})
|
||||
.then((exist) => {
|
||||
if (!exist.body) {
|
||||
client.indices.create({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
body: index,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default client;
|
|
@ -1,451 +0,0 @@
|
|||
import { Health, Index, MeiliSearch, Stats } from "meilisearch";
|
||||
import { dbLogger } from "./logger.js";
|
||||
|
||||
import config from "@/config/index.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import * as url from "url";
|
||||
import { ILocalUser } from "@/models/entities/user.js";
|
||||
import { Followings, Users } from "@/models/index.js";
|
||||
|
||||
const logger = dbLogger.createSubLogger("meilisearch", "gray", false);
|
||||
|
||||
let posts: Index;
|
||||
let client: MeiliSearch;
|
||||
|
||||
const hasConfig =
|
||||
config.meilisearch &&
|
||||
(config.meilisearch.host ||
|
||||
config.meilisearch.port ||
|
||||
config.meilisearch.apiKey);
|
||||
|
||||
if (hasConfig) {
|
||||
const host = hasConfig ? config.meilisearch.host ?? "localhost" : "";
|
||||
const port = hasConfig ? config.meilisearch.port ?? 7700 : 0;
|
||||
const auth = hasConfig ? config.meilisearch.apiKey ?? "" : "";
|
||||
const ssl = hasConfig ? config.meilisearch.ssl ?? false : false;
|
||||
|
||||
logger.info("Connecting to MeiliSearch");
|
||||
|
||||
client = new MeiliSearch({
|
||||
host: `${ssl ? "https" : "http"}://${host}:${port}`,
|
||||
apiKey: auth,
|
||||
});
|
||||
|
||||
posts = client.index("posts");
|
||||
|
||||
posts
|
||||
.updateSearchableAttributes(["text"])
|
||||
.catch((e) =>
|
||||
logger.error(`Setting searchable attr failed, searches won't work: ${e}`),
|
||||
);
|
||||
|
||||
posts
|
||||
.updateFilterableAttributes([
|
||||
"userName",
|
||||
"userHost",
|
||||
"mediaAttachment",
|
||||
"createdAt",
|
||||
"userId",
|
||||
])
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
`Setting filterable attr failed, advanced searches won't work: ${e}`,
|
||||
),
|
||||
);
|
||||
|
||||
posts
|
||||
.updateSortableAttributes(["createdAt"])
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
`Setting sortable attr failed, placeholder searches won't sort properly: ${e}`,
|
||||
),
|
||||
);
|
||||
|
||||
posts
|
||||
.updateStopWords([
|
||||
"the",
|
||||
"a",
|
||||
"as",
|
||||
"be",
|
||||
"of",
|
||||
"they",
|
||||
"these",
|
||||
"is",
|
||||
"are",
|
||||
"これ",
|
||||
"それ",
|
||||
"あれ",
|
||||
"この",
|
||||
"その",
|
||||
"あの",
|
||||
"ここ",
|
||||
"そこ",
|
||||
"あそこ",
|
||||
"こちら",
|
||||
"どこ",
|
||||
"私",
|
||||
"僕",
|
||||
"俺",
|
||||
"君",
|
||||
"あなた",
|
||||
"我々",
|
||||
"私達",
|
||||
"彼女",
|
||||
"彼",
|
||||
"です",
|
||||
"ます",
|
||||
"は",
|
||||
"が",
|
||||
"の",
|
||||
"に",
|
||||
"を",
|
||||
"で",
|
||||
"へ",
|
||||
"から",
|
||||
"まで",
|
||||
"より",
|
||||
"も",
|
||||
"どの",
|
||||
"と",
|
||||
"それで",
|
||||
"しかし",
|
||||
])
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
`Failed to set Meilisearch stop words, database size will be larger: ${e}`,
|
||||
),
|
||||
);
|
||||
|
||||
posts
|
||||
.updateRankingRules([
|
||||
"sort",
|
||||
"words",
|
||||
"typo",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness",
|
||||
])
|
||||
.catch((e) => {
|
||||
logger.error("Failed to set ranking rules, sorting won't work properly.");
|
||||
});
|
||||
|
||||
logger.info("Connected to MeiliSearch");
|
||||
}
|
||||
|
||||
export type MeilisearchNote = {
|
||||
id: string;
|
||||
text: string;
|
||||
userId: string;
|
||||
userHost: string;
|
||||
userName: string;
|
||||
channelId: string;
|
||||
mediaAttachment: string;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
function timestampToUnix(timestamp: string) {
|
||||
let unix = 0;
|
||||
|
||||
// Only contains numbers => UNIX timestamp
|
||||
if (/^\d+$/.test(timestamp)) {
|
||||
unix = Number.parseInt(timestamp);
|
||||
}
|
||||
|
||||
if (unix === 0) {
|
||||
// Try to parse the timestamp as JavaScript Date
|
||||
const date = Date.parse(timestamp);
|
||||
if (Number.isNaN(date)) return 0;
|
||||
unix = date / 1000;
|
||||
}
|
||||
|
||||
return unix;
|
||||
}
|
||||
|
||||
export default hasConfig
|
||||
? {
|
||||
search: async (
|
||||
query: string,
|
||||
limit: number,
|
||||
offset: number,
|
||||
userCtx: ILocalUser | null,
|
||||
overrideSort: string | null,
|
||||
) => {
|
||||
/// Advanced search syntax
|
||||
/// from:user => filter by user + optional domain
|
||||
/// has:image/video/audio/text/file => filter by attachment types
|
||||
/// domain:domain.com => filter by domain
|
||||
/// before:Date => show posts made before Date
|
||||
/// after: Date => show posts made after Date
|
||||
/// "text" => get posts with exact text between quotes
|
||||
/// filter:following => show results only from users you follow
|
||||
/// filter:followers => show results only from followers
|
||||
/// order:desc/asc => order results ascending or descending
|
||||
|
||||
const constructedFilters: string[] = [];
|
||||
let sortRules: string[] = [];
|
||||
|
||||
const splitSearch = query.split(" ");
|
||||
|
||||
// Detect search operators and remove them from the actual query
|
||||
const filteredSearchTerms = (
|
||||
await Promise.all(
|
||||
splitSearch.map(async (term) => {
|
||||
if (term.startsWith("has:")) {
|
||||
const fileType = term.slice(4);
|
||||
constructedFilters.push(`mediaAttachment = "${fileType}"`);
|
||||
return null;
|
||||
} else if (term.startsWith("from:")) {
|
||||
let user = term.slice(5);
|
||||
|
||||
if (user.length === 0) return null;
|
||||
|
||||
// Cut off leading @, those aren't saved in the DB
|
||||
if (user.charAt(0) === "@") {
|
||||
user = user.slice(1);
|
||||
}
|
||||
|
||||
// Determine if we got a webfinger address or a single username
|
||||
if (user.split("@").length > 1) {
|
||||
const splitUser = user.split("@");
|
||||
|
||||
const domain = splitUser.pop();
|
||||
user = splitUser.join("@");
|
||||
|
||||
constructedFilters.push(
|
||||
`userName = ${user} AND userHost = ${domain}`,
|
||||
);
|
||||
} else {
|
||||
constructedFilters.push(`userName = ${user}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("domain:")) {
|
||||
const domain = term.slice(7);
|
||||
if (
|
||||
domain.length === 0 ||
|
||||
domain === "local" ||
|
||||
domain === config.hostname
|
||||
) {
|
||||
constructedFilters.push("userHost NOT EXISTS");
|
||||
return null;
|
||||
}
|
||||
constructedFilters.push(`userHost = ${domain}`);
|
||||
return null;
|
||||
} else if (term.startsWith("after:")) {
|
||||
const timestamp = term.slice(6);
|
||||
|
||||
const unix = timestampToUnix(timestamp);
|
||||
|
||||
if (unix !== 0) constructedFilters.push(`createdAt > ${unix}`);
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("before:")) {
|
||||
const timestamp = term.slice(7);
|
||||
|
||||
const unix = timestampToUnix(timestamp);
|
||||
if (unix !== 0) constructedFilters.push(`createdAt < ${unix}`);
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("filter:following")) {
|
||||
// Check if we got a context user
|
||||
if (userCtx) {
|
||||
// Fetch user follows from DB
|
||||
const followedUsers = await Followings.find({
|
||||
where: {
|
||||
followerId: userCtx.id,
|
||||
},
|
||||
select: {
|
||||
followeeId: true,
|
||||
},
|
||||
});
|
||||
const followIDs = followedUsers.map(
|
||||
(user) => user.followeeId,
|
||||
);
|
||||
|
||||
if (followIDs.length === 0) return null;
|
||||
|
||||
constructedFilters.push(`userId IN [${followIDs.join(",")}]`);
|
||||
} else {
|
||||
logger.warn(
|
||||
"search filtered to follows called without user context",
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("filter:followers")) {
|
||||
// Check if we got a context user
|
||||
if (userCtx) {
|
||||
// Fetch users follows from DB
|
||||
const followedUsers = await Followings.find({
|
||||
where: {
|
||||
followeeId: userCtx.id,
|
||||
},
|
||||
select: {
|
||||
followerId: true,
|
||||
},
|
||||
});
|
||||
const followIDs = followedUsers.map(
|
||||
(user) => user.followerId,
|
||||
);
|
||||
|
||||
if (followIDs.length === 0) return null;
|
||||
|
||||
constructedFilters.push(`userId IN [${followIDs.join(",")}]`);
|
||||
} else {
|
||||
logger.warn(
|
||||
"search filtered to followers called without user context",
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("order:desc")) {
|
||||
sortRules.push("createdAt:desc");
|
||||
|
||||
return null;
|
||||
} else if (term.startsWith("order:asc")) {
|
||||
sortRules.push("createdAt:asc");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return term;
|
||||
}),
|
||||
)
|
||||
).filter((term) => term !== null);
|
||||
|
||||
// An empty search term with defined filters means we have a placeholder search => https://www.meilisearch.com/docs/reference/api/search#placeholder-search
|
||||
// These have to be ordered manually, otherwise the *oldest* posts are returned first, which we don't want
|
||||
// If the user has defined a sort rule, don't mess with it
|
||||
if (
|
||||
filteredSearchTerms.length === 0 &&
|
||||
constructedFilters.length > 0 &&
|
||||
sortRules.length === 0
|
||||
) {
|
||||
sortRules.push("createdAt:desc");
|
||||
}
|
||||
|
||||
// More than one sorting rule doesn't make sense. We only keep the first one, otherwise weird stuff may happen.
|
||||
if (sortRules.length > 1) {
|
||||
sortRules = [sortRules[0]];
|
||||
}
|
||||
|
||||
// An override sort takes precedence, user sorting is ignored here
|
||||
if (overrideSort) {
|
||||
sortRules = [overrideSort];
|
||||
}
|
||||
|
||||
logger.info(`Searching for ${filteredSearchTerms.join(" ")}`);
|
||||
logger.info(`Limit: ${limit}`);
|
||||
logger.info(`Offset: ${offset}`);
|
||||
logger.info(`Filters: ${constructedFilters}`);
|
||||
logger.info(`Ordering: ${sortRules}`);
|
||||
|
||||
return posts.search(filteredSearchTerms.join(" "), {
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
filter: constructedFilters,
|
||||
sort: sortRules,
|
||||
});
|
||||
},
|
||||
ingestNote: async (ingestNotes: Note | Note[]) => {
|
||||
if (ingestNotes instanceof Note) {
|
||||
ingestNotes = [ingestNotes];
|
||||
}
|
||||
|
||||
const indexingBatch: MeilisearchNote[] = [];
|
||||
|
||||
for (const note of ingestNotes) {
|
||||
if (note.user === undefined) {
|
||||
note.user = await Users.findOne({
|
||||
where: {
|
||||
id: note.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let attachmentType = "";
|
||||
if (note.attachedFileTypes.length > 0) {
|
||||
attachmentType = note.attachedFileTypes[0].split("/")[0];
|
||||
switch (attachmentType) {
|
||||
case "image":
|
||||
case "video":
|
||||
case "audio":
|
||||
case "text":
|
||||
break;
|
||||
default:
|
||||
attachmentType = "file";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
indexingBatch.push(<MeilisearchNote>{
|
||||
id: note.id.toString(),
|
||||
text: note.text ? note.text : "",
|
||||
userId: note.userId,
|
||||
userHost:
|
||||
note.userHost !== ""
|
||||
? note.userHost
|
||||
: url.parse(config.host).host,
|
||||
channelId: note.channelId ? note.channelId : "",
|
||||
mediaAttachment: attachmentType,
|
||||
userName: note.user?.username ?? "UNKNOWN",
|
||||
createdAt: note.createdAt.getTime() / 1000, // division by 1000 is necessary because Node returns in ms-accuracy
|
||||
});
|
||||
}
|
||||
|
||||
return posts
|
||||
.addDocuments(indexingBatch, {
|
||||
primaryKey: "id",
|
||||
})
|
||||
.then(() =>
|
||||
logger.info(`sent ${indexingBatch.length} posts for indexing`),
|
||||
);
|
||||
},
|
||||
serverStats: async () => {
|
||||
const health: Health = await client.health();
|
||||
const stats: Stats = await client.getStats();
|
||||
|
||||
return {
|
||||
health: health.status,
|
||||
size: stats.databaseSize,
|
||||
indexed_count: stats.indexes["posts"].numberOfDocuments,
|
||||
};
|
||||
},
|
||||
deleteNotes: async (note: Note | Note[] | string | string[]) => {
|
||||
if (note instanceof Note) {
|
||||
note = [note];
|
||||
}
|
||||
if (typeof note === "string") {
|
||||
note = [note];
|
||||
}
|
||||
|
||||
const deletionBatch = note
|
||||
.map((n) => {
|
||||
if (n instanceof Note) {
|
||||
return n.id;
|
||||
}
|
||||
|
||||
if (n.length > 0) return n;
|
||||
|
||||
logger.error(
|
||||
`Failed to delete note from Meilisearch, invalid post ID: ${JSON.stringify(
|
||||
n,
|
||||
)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Invalid note ID passed to meilisearch deleteNote: ${JSON.stringify(
|
||||
n,
|
||||
)}`,
|
||||
);
|
||||
})
|
||||
.filter((el) => el !== null);
|
||||
|
||||
await posts.deleteDocuments(deletionBatch as string[]).then(() => {
|
||||
logger.info(
|
||||
`submitted ${deletionBatch.length} large batch for deletion`,
|
||||
);
|
||||
});
|
||||
},
|
||||
}
|
||||
: null;
|
|
@ -1,51 +0,0 @@
|
|||
import * as SonicChannel from "sonic-channel";
|
||||
import { dbLogger } from "./logger.js";
|
||||
|
||||
import config from "@/config/index.js";
|
||||
|
||||
const logger = dbLogger.createSubLogger("sonic", "gray", false);
|
||||
|
||||
const handlers = (type: string): SonicChannel.Handlers => ({
|
||||
connected: () => {
|
||||
logger.succ(`Connected to Sonic ${type}`);
|
||||
},
|
||||
disconnected: (error) => {
|
||||
logger.warn(`Disconnected from Sonic ${type}, error: ${error}`);
|
||||
},
|
||||
error: (error) => {
|
||||
logger.warn(`Sonic ${type} error: ${error}`);
|
||||
},
|
||||
retrying: () => {
|
||||
logger.info(`Sonic ${type} retrying`);
|
||||
},
|
||||
timeout: () => {
|
||||
logger.warn(`Sonic ${type} timeout`);
|
||||
},
|
||||
});
|
||||
|
||||
const hasConfig =
|
||||
config.sonic && (config.sonic.host || config.sonic.port || config.sonic.auth);
|
||||
|
||||
if (hasConfig) {
|
||||
logger.info("Connecting to Sonic");
|
||||
}
|
||||
|
||||
const host = hasConfig ? config.sonic.host ?? "localhost" : "";
|
||||
const port = hasConfig ? config.sonic.port ?? 1491 : 0;
|
||||
const auth = hasConfig ? config.sonic.auth ?? "SecretPassword" : "";
|
||||
const collection = hasConfig ? config.sonic.collection ?? "main" : "";
|
||||
const bucket = hasConfig ? config.sonic.bucket ?? "default" : "";
|
||||
|
||||
export default hasConfig
|
||||
? {
|
||||
search: new SonicChannel.Search({ host, port, auth }).connect(
|
||||
handlers("search"),
|
||||
),
|
||||
ingest: new SonicChannel.Ingest({ host, port, auth }).connect(
|
||||
handlers("ingest"),
|
||||
),
|
||||
|
||||
collection,
|
||||
bucket,
|
||||
}
|
||||
: null;
|
|
@ -70,6 +70,7 @@ export class DriveFile {
|
|||
})
|
||||
public size: number;
|
||||
|
||||
@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
|
||||
@Column("varchar", {
|
||||
length: DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||
nullable: true,
|
||||
|
|
|
@ -61,6 +61,7 @@ export class Note {
|
|||
})
|
||||
public threadId: string | null;
|
||||
|
||||
@Index() // USING pgroonga
|
||||
@Column("text", {
|
||||
nullable: true,
|
||||
})
|
||||
|
@ -78,6 +79,7 @@ export class Note {
|
|||
})
|
||||
public name: string | null;
|
||||
|
||||
@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -38,6 +38,7 @@ export class UserProfile {
|
|||
})
|
||||
public birthday: string | null;
|
||||
|
||||
@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
|
||||
@Column("varchar", {
|
||||
length: 2048,
|
||||
nullable: true,
|
||||
|
|
|
@ -58,6 +58,7 @@ export class User {
|
|||
})
|
||||
public usernameLower: string;
|
||||
|
||||
@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
|
|
|
@ -13,7 +13,6 @@ import processDb from "./processors/db/index.js";
|
|||
import processObjectStorage from "./processors/object-storage/index.js";
|
||||
import processSystemQueue from "./processors/system/index.js";
|
||||
import processWebhookDeliver from "./processors/webhook-deliver.js";
|
||||
import processBackground from "./processors/background/index.js";
|
||||
import { endedPollNotification } from "./processors/ended-poll-notification.js";
|
||||
import { queueLogger } from "./logger.js";
|
||||
import { getJobInfo } from "./get-job-info.js";
|
||||
|
@ -482,14 +481,6 @@ export function createCleanRemoteFilesJob() {
|
|||
);
|
||||
}
|
||||
|
||||
export function createIndexAllNotesJob(data = {}) {
|
||||
return backgroundQueue.add("indexAllNotes", data, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
timeout: 1000 * 60 * 60 * 24,
|
||||
});
|
||||
}
|
||||
|
||||
export function webhookDeliver(
|
||||
webhook: Webhook,
|
||||
type: (typeof webhookEventTypes)[number],
|
||||
|
@ -526,7 +517,6 @@ export default function () {
|
|||
webhookDeliverQueue.process(64, processWebhookDeliver);
|
||||
processDb(dbQueue);
|
||||
processObjectStorage(objectStorageQueue);
|
||||
processBackground(backgroundQueue);
|
||||
|
||||
systemQueue.add(
|
||||
"cleanCharts",
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
import type Bull from "bull";
|
||||
import type { DoneCallback } from "bull";
|
||||
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import { Notes } from "@/models/index.js";
|
||||
import { MoreThan } from "typeorm";
|
||||
import { index } from "@/services/note/create.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const logger = queueLogger.createSubLogger("index-all-notes");
|
||||
|
||||
export default async function indexAllNotes(
|
||||
job: Bull.Job<Record<string, unknown>>,
|
||||
done: DoneCallback,
|
||||
): Promise<void> {
|
||||
logger.info("Indexing all notes...");
|
||||
|
||||
let cursor: string | null = (job.data.cursor as string) ?? null;
|
||||
let indexedCount: number = (job.data.indexedCount as number) ?? 0;
|
||||
let total: number = (job.data.total as number) ?? 0;
|
||||
|
||||
let running = true;
|
||||
const take = 10000;
|
||||
const batch = 100;
|
||||
while (running) {
|
||||
logger.info(
|
||||
`Querying for ${take} notes ${indexedCount}/${
|
||||
total ? total : "?"
|
||||
} at ${cursor}`,
|
||||
);
|
||||
|
||||
let notes: Note[] = [];
|
||||
try {
|
||||
notes = await Notes.find({
|
||||
where: {
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: take,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ["user"],
|
||||
});
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to query notes:\n${inspect(e)}`);
|
||||
done(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
await job.progress(100);
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const count = await Notes.count();
|
||||
total = count;
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
} catch (e) {}
|
||||
|
||||
for (let i = 0; i < notes.length; i += batch) {
|
||||
const chunk = notes.slice(i, i + batch);
|
||||
|
||||
if (meilisearch) {
|
||||
await meilisearch.ingestNote(chunk);
|
||||
}
|
||||
|
||||
await Promise.all(chunk.map((note) => index(note, true)));
|
||||
|
||||
indexedCount += chunk.length;
|
||||
const pct = (indexedCount / total) * 100;
|
||||
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;
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
|
||||
if (notes.length < take) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
logger.info("All notes have been indexed.");
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import type Bull from "bull";
|
||||
import indexAllNotes from "./index-all-notes.js";
|
||||
|
||||
const jobs = {
|
||||
indexAllNotes,
|
||||
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>>>;
|
||||
|
||||
export default function (q: Bull.Queue) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
q.process(k, 16, v);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
|
|||
import { MoreThan } from "typeorm";
|
||||
import { deleteFileSync } from "@/services/drive/delete-file.js";
|
||||
import { sendEmail } from "@/services/send-email.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
|
||||
const logger = queueLogger.createSubLogger("delete-account");
|
||||
|
||||
|
@ -42,9 +41,6 @@ export async function deleteAccount(
|
|||
cursor = notes[notes.length - 1].id;
|
||||
|
||||
await Notes.delete(notes.map((note) => note.id));
|
||||
if (meilisearch) {
|
||||
await meilisearch.deleteNotes(notes);
|
||||
}
|
||||
}
|
||||
|
||||
logger.succ("All of notes deleted");
|
||||
|
|
|
@ -51,7 +51,6 @@ import * as ep___admin_relays_list from "./endpoints/admin/relays/list.js";
|
|||
import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js";
|
||||
import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js";
|
||||
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js";
|
||||
import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js";
|
||||
import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js";
|
||||
import * as ep___admin_sendModMail from "./endpoints/admin/send-mod-mail.js";
|
||||
import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js";
|
||||
|
@ -400,7 +399,6 @@ const eps = [
|
|||
["admin/relays/remove", ep___admin_relays_remove],
|
||||
["admin/reset-password", ep___admin_resetPassword],
|
||||
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
|
||||
["admin/search/index-all", ep___admin_search_indexAll],
|
||||
["admin/send-email", ep___admin_sendEmail],
|
||||
["admin/send-mod-mail", ep___admin_sendModMail],
|
||||
["admin/server-info", ep___admin_serverInfo],
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { createIndexAllNotesJob } from "@/queue/index.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
cursor: {
|
||||
type: "string",
|
||||
format: "misskey:id",
|
||||
nullable: true,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, _me) => {
|
||||
createIndexAllNotesJob({
|
||||
cursor: ps.cursor ?? undefined,
|
||||
});
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
import { In } from "typeorm";
|
||||
import { index } from "@/services/note/create.js";
|
||||
import type { IRemoteUser, User } from "@/models/entities/user.js";
|
||||
import {
|
||||
Users,
|
||||
|
@ -626,8 +625,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
if (publishing && user.isIndexable) {
|
||||
index(note, true);
|
||||
|
||||
// Publish update event for the updated note details
|
||||
publishNoteStream(note.id, "updated", {
|
||||
updatedAt: update.updatedAt,
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { FindManyOptions, In } from "typeorm";
|
||||
import { Notes } from "@/models/index.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import config from "@/config/index.js";
|
||||
import es from "@/db/elasticsearch.js";
|
||||
import sonic from "@/db/sonic.js";
|
||||
import meilisearch, { MeilisearchNote } from "@/db/meilisearch.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
|
||||
|
@ -69,7 +64,6 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
if (es == null && sonic == null && meilisearch == null) {
|
||||
const query = makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
ps.sinceId,
|
||||
|
@ -87,7 +81,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
|
||||
query
|
||||
.andWhere("note.text ILIKE :q", { q: `%${sqlLikeEscape(ps.query)}%` })
|
||||
.andWhere("note.text &@~ :q", { q: `${sqlLikeEscape(ps.query)}` })
|
||||
.andWhere("note.visibility = 'public'")
|
||||
.innerJoinAndSelect("note.user", "user")
|
||||
.andWhere("user.isIndexable = TRUE")
|
||||
|
@ -109,242 +103,4 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const notes: Note[] = await query.take(ps.limit).getMany();
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
} else if (sonic) {
|
||||
let start = 0;
|
||||
const chunkSize = 100;
|
||||
|
||||
// Use sonic to fetch and step through all search results that could match the requirements
|
||||
const ids = [];
|
||||
while (true) {
|
||||
const results = await sonic.search.query(
|
||||
sonic.collection,
|
||||
sonic.bucket,
|
||||
ps.query,
|
||||
{
|
||||
limit: chunkSize,
|
||||
offset: start,
|
||||
},
|
||||
);
|
||||
|
||||
start += chunkSize;
|
||||
|
||||
if (results.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const res = results
|
||||
.map((k) => JSON.parse(k))
|
||||
.filter((key) => {
|
||||
if (ps.userId && key.userId !== ps.userId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.channelId && key.channelId !== ps.channelId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.sinceId && key.id <= ps.sinceId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.untilId && key.id >= ps.untilId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((key) => key.id);
|
||||
|
||||
ids.push(...res);
|
||||
}
|
||||
|
||||
// Sort all the results by note id DESC (newest first)
|
||||
ids.sort((a, b) => b - a);
|
||||
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
while (found.length < ps.limit && start < ids.length) {
|
||||
const chunk = ids.slice(start, start + chunkSize);
|
||||
const notes: Note[] = await Notes.find({
|
||||
where: {
|
||||
id: In(chunk),
|
||||
},
|
||||
});
|
||||
|
||||
// The notes are checked for visibility and muted/blocked users when packed
|
||||
found.push(...(await Notes.packMany(notes, me)));
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
// If we have more results than the limit, trim them
|
||||
if (found.length > ps.limit) {
|
||||
found.length = ps.limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
} else if (meilisearch) {
|
||||
let start = 0;
|
||||
const chunkSize = 100;
|
||||
const sortByDate = ps.order !== "relevancy";
|
||||
|
||||
type NoteResult = {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
};
|
||||
const extractedNotes: NoteResult[] = [];
|
||||
|
||||
while (true) {
|
||||
const searchRes = await meilisearch.search(
|
||||
ps.query,
|
||||
chunkSize,
|
||||
start,
|
||||
me,
|
||||
sortByDate ? "createdAt:desc" : null,
|
||||
);
|
||||
const results: MeilisearchNote[] = searchRes.hits as MeilisearchNote[];
|
||||
|
||||
start += chunkSize;
|
||||
|
||||
if (results.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const res = results
|
||||
.filter((key: MeilisearchNote) => {
|
||||
if (ps.userId && key.userId !== ps.userId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.channelId && key.channelId !== ps.channelId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.sinceId && key.id <= ps.sinceId) {
|
||||
return false;
|
||||
}
|
||||
if (ps.untilId && key.id >= ps.untilId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((key) => {
|
||||
return {
|
||||
id: key.id,
|
||||
createdAt: key.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
extractedNotes.push(...res);
|
||||
}
|
||||
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
const noteIDs = extractedNotes.map((note) => note.id);
|
||||
|
||||
// Index the ID => index number into a map, so we can restore the array ordering efficiently later
|
||||
const idIndexMap = new Map(noteIDs.map((id, index) => [id, index]));
|
||||
|
||||
while (found.length < ps.limit && start < noteIDs.length) {
|
||||
const chunk = noteIDs.slice(start, start + chunkSize);
|
||||
|
||||
let query: FindManyOptions = {
|
||||
where: {
|
||||
id: In(chunk),
|
||||
},
|
||||
};
|
||||
|
||||
const notes: Note[] = await Notes.find(query);
|
||||
|
||||
// Re-order the note result according to the noteIDs array (cannot be undefined, we map this earlier)
|
||||
// @ts-ignore
|
||||
notes.sort((a, b) => idIndexMap.get(a.id) - idIndexMap.get(b.id));
|
||||
|
||||
// The notes are checked for visibility and muted/blocked users when packed
|
||||
found.push(...(await Notes.packMany(notes, me)));
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
// If we have more results than the limit, trim the results down
|
||||
if (found.length > ps.limit) {
|
||||
found.length = ps.limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
} else {
|
||||
const userQuery =
|
||||
ps.userId != null
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
userId: ps.userId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const hostQuery =
|
||||
ps.userId == null
|
||||
? ps.host === null
|
||||
? [
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: "userHost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: ps.host !== undefined
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
userHost: ps.host,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []
|
||||
: [];
|
||||
|
||||
const result = await es.search({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
body: {
|
||||
size: ps.limit,
|
||||
from: ps.offset,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
fields: ["text"],
|
||||
query: ps.query.toLowerCase(),
|
||||
default_operator: "and",
|
||||
},
|
||||
},
|
||||
...hostQuery,
|
||||
...userQuery,
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
_doc: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||
|
||||
if (hits.length === 0) return [];
|
||||
|
||||
// Fetch found notes
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
id: In(hits),
|
||||
},
|
||||
order: {
|
||||
id: -1,
|
||||
},
|
||||
});
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as os from "node:os";
|
||||
import si from "systeminformation";
|
||||
import define from "@/server/api/define.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
|
||||
export const meta = {
|
||||
|
@ -63,15 +62,3 @@ export default define(meta, paramDef, async () => {
|
|||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function meilisearchStatus() {
|
||||
if (meilisearch) {
|
||||
return meilisearch.serverStats();
|
||||
} else {
|
||||
return {
|
||||
health: "unconfigured",
|
||||
size: 0,
|
||||
indexed_count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,8 +78,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const nameQuery = Users.createQueryBuilder("user")
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where("user.name ILIKE :query", {
|
||||
query: `%${sqlLikeEscape(ps.query)}%`,
|
||||
qb.where("user.name &@~ :query", {
|
||||
query: `${sqlLikeEscape(ps.query)}`,
|
||||
});
|
||||
|
||||
// Also search username if it qualifies as username
|
||||
|
@ -115,8 +115,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
if (users.length < ps.limit) {
|
||||
const profQuery = UserProfiles.createQueryBuilder("prof")
|
||||
.select("prof.userId")
|
||||
.where("prof.description ILIKE :query", {
|
||||
query: `%${sqlLikeEscape(ps.query)}%`,
|
||||
.where("prof.description &@~ :query", {
|
||||
query: `${sqlLikeEscape(ps.query)}`,
|
||||
});
|
||||
|
||||
if (ps.origin === "local") {
|
||||
|
|
|
@ -82,7 +82,6 @@ const nodeinfo2 = async () => {
|
|||
disableRecommendedTimeline: meta.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||
searchFilters: config.meilisearch ? true : false,
|
||||
postEditing: true,
|
||||
postImports: meta.experimentalFeatures?.postImports || false,
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import * as mfm from "mfm-js";
|
||||
import es from "@/db/elasticsearch.js";
|
||||
import sonic from "@/db/sonic.js";
|
||||
import {
|
||||
publishMainStream,
|
||||
publishNotesStream,
|
||||
|
@ -59,7 +57,6 @@ import type { UserProfile } from "@/models/entities/user-profile.js";
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
import { Mutex } from "redis-semaphore";
|
||||
import { langmap } from "@/misc/langmap.js";
|
||||
|
@ -166,7 +163,6 @@ export default async (
|
|||
createdAt: User["createdAt"];
|
||||
isBot: User["isBot"];
|
||||
inbox?: User["inbox"];
|
||||
isIndexable?: User["isIndexable"];
|
||||
},
|
||||
data: Option,
|
||||
silent = false,
|
||||
|
@ -654,11 +650,6 @@ export default async (
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register to search database
|
||||
if (user.isIndexable) {
|
||||
await index(note, false);
|
||||
}
|
||||
});
|
||||
|
||||
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
||||
|
@ -814,40 +805,6 @@ async function insertNote(
|
|||
}
|
||||
}
|
||||
|
||||
export async function index(note: Note, reindexing: boolean): Promise<void> {
|
||||
if (!note.text || note.visibility !== "public") return;
|
||||
|
||||
if (config.elasticsearch && es) {
|
||||
es.index({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
id: note.id.toString(),
|
||||
body: {
|
||||
text: normalizeForSearch(note.text),
|
||||
userId: note.userId,
|
||||
userHost: note.userHost,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (sonic) {
|
||||
await sonic.ingest.push(
|
||||
sonic.collection,
|
||||
sonic.bucket,
|
||||
JSON.stringify({
|
||||
id: note.id,
|
||||
userId: note.userId,
|
||||
userHost: note.userHost,
|
||||
channelId: note.channelId,
|
||||
}),
|
||||
note.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (meilisearch && !reindexing) {
|
||||
await meilisearch.ingestNote(note);
|
||||
}
|
||||
}
|
||||
|
||||
async function notifyToWatchersOfRenotee(
|
||||
renote: Note,
|
||||
user: { id: User["id"] },
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
import { countSameRenotes } from "@/misc/count-same-renotes.js";
|
||||
import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js";
|
||||
import { deliverToRelays } from "@/services/relay.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
|
||||
/**
|
||||
* 投稿を削除します。
|
||||
|
@ -117,10 +116,6 @@ export default async function (
|
|||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (meilisearch) {
|
||||
await meilisearch.deleteNotes(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function findCascadingNotes(note: Note) {
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
:display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<FormButton primary @click="indexPosts">{{
|
||||
i18n.ts.indexPosts
|
||||
}}</FormButton>
|
||||
<FormSuspense
|
||||
v-slot="{ result: database }"
|
||||
:p="databasePromiseFactory"
|
||||
|
@ -44,7 +41,6 @@ import bytes from "@/filters/bytes";
|
|||
import number from "@/filters/number";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { indexPosts } from "@/scripts/index-posts";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const databasePromiseFactory = () =>
|
||||
|
|
|
@ -78,7 +78,6 @@ import * as os from "@/os";
|
|||
import { lookupUser } from "@/scripts/lookup-user";
|
||||
import { lookupFile } from "@/scripts/lookup-file";
|
||||
import { lookupInstance } from "@/scripts/lookup-instance";
|
||||
import { indexPosts } from "@/scripts/index-posts";
|
||||
import { defaultStore } from "@/store";
|
||||
import { useRouter } from "@/router";
|
||||
import {
|
||||
|
@ -156,16 +155,6 @@ const menuDef = computed(() => [
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...($i.isAdmin
|
||||
? [
|
||||
{
|
||||
type: "button",
|
||||
icon: `${icon("ph-list-magnifying-glass")}`,
|
||||
text: i18n.ts.indexPosts,
|
||||
action: indexPosts,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -29,24 +29,6 @@
|
|||
<p>Used: {{ bytes(diskUsed, 1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_panel">
|
||||
<XPie class="pie" :value="meiliProgress" />
|
||||
<div>
|
||||
<p><i :class="icon('ph-file-search')"></i>MeiliSearch</p>
|
||||
<p>
|
||||
{{ i18n.ts._widgets.meiliStatus }}: {{ meiliAvailable }}
|
||||
</p>
|
||||
<p>
|
||||
{{ i18n.ts._widgets.meiliSize }}:
|
||||
{{ bytes(meiliTotalSize, 1) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ i18n.ts._widgets.meiliIndexCount }}:
|
||||
{{ meiliIndexCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -57,7 +39,6 @@ import XPie from "../../widgets/server-metric/pie.vue";
|
|||
import bytes from "@/filters/bytes";
|
||||
import { useStream } from "@/stream";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const stream = useStream();
|
||||
|
@ -72,11 +53,6 @@ const memTotal = ref(0);
|
|||
const memUsed = ref(0);
|
||||
const memFree = ref(0);
|
||||
|
||||
const meiliProgress = ref(0);
|
||||
const meiliTotalSize = ref(0);
|
||||
const meiliIndexCount = ref(0);
|
||||
const meiliAvailable = ref("unavailable");
|
||||
|
||||
const diskUsage = computed(() => meta.fs.used / meta.fs.total);
|
||||
const diskTotal = computed(() => meta.fs.total);
|
||||
const diskUsed = computed(() => meta.fs.used);
|
||||
|
@ -89,11 +65,6 @@ function onStats(stats) {
|
|||
memTotal.value = stats.mem.total;
|
||||
memUsed.value = stats.mem.active;
|
||||
memFree.value = memTotal.value - memUsed.value;
|
||||
|
||||
meiliTotalSize.value = stats.meilisearch.size;
|
||||
meiliIndexCount.value = stats.meilisearch.indexed_count;
|
||||
meiliAvailable.value = stats.meilisearch.health;
|
||||
meiliProgress.value = meiliIndexCount.value / serverStats.notesCount;
|
||||
}
|
||||
|
||||
const connection = stream.useChannel("serverStats");
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
|
||||
export async function indexPosts() {
|
||||
const { canceled, result: index } = await os.inputText({
|
||||
title: i18n.ts.indexFrom,
|
||||
text: i18n.ts.indexFromDescription,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
if (index == null || index === "") {
|
||||
await os.api("admin/search/index-all");
|
||||
await os.alert({
|
||||
type: "info",
|
||||
text: i18n.ts.indexNotice,
|
||||
});
|
||||
} else {
|
||||
await os.api("admin/search/index-all", {
|
||||
cursor: index,
|
||||
});
|
||||
await os.alert({
|
||||
type: "info",
|
||||
text: i18n.ts.indexNotice,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -46,13 +46,6 @@
|
|||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XMeili
|
||||
v-else-if="
|
||||
instance.features.searchFilters && widgetProps.view === 5
|
||||
"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
@ -66,7 +59,6 @@ import XNet from "./net.vue";
|
|||
import XCpu from "./cpu.vue";
|
||||
import XMemory from "./mem.vue";
|
||||
import XDisk from "./disk.vue";
|
||||
import XMeili from "./meilisearch.vue";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import type { GetFormResultType } from "@/scripts/form";
|
||||
import * as os from "@/os";
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
<template>
|
||||
<div class="verusivbr">
|
||||
<XPie
|
||||
v-tooltip="i18n.ts.meiliIndexCount"
|
||||
class="pie"
|
||||
:value="progress"
|
||||
:reverse="true"
|
||||
/>
|
||||
<div>
|
||||
<p><i :class="icon('ph-file-search')"></i>MeiliSearch</p>
|
||||
<p>{{ i18n.ts._widgets.meiliStatus }}: {{ available }}</p>
|
||||
<p>{{ i18n.ts._widgets.meiliSize }}: {{ bytes(totalSize, 1) }}</p>
|
||||
<p>{{ i18n.ts._widgets.meiliIndexCount }}: {{ indexCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import XPie from "./pie.vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
const progress = ref<number>(0);
|
||||
const serverStats = ref(null);
|
||||
const totalSize = ref<number>(0);
|
||||
const indexCount = ref<number>(0);
|
||||
const available = ref<string>("unavailable");
|
||||
|
||||
function onStats(stats) {
|
||||
totalSize.value = stats.meilisearch.size;
|
||||
indexCount.value = stats.meilisearch.indexed_count;
|
||||
available.value = stats.meilisearch.health;
|
||||
progress.value = indexCount.value / serverStats.value.notesCount;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
os.api("stats", {}).then((res) => {
|
||||
serverStats.value = res;
|
||||
});
|
||||
props.connection.on("stats", onStats);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.verusivbr {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -75,9 +75,6 @@ importers:
|
|||
'@discordapp/twemoji':
|
||||
specifier: ^15.0.2
|
||||
version: 15.0.2
|
||||
'@elastic/elasticsearch':
|
||||
specifier: 8.12.2
|
||||
version: 8.12.2
|
||||
'@koa/cors':
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0
|
||||
|
@ -243,9 +240,6 @@ importers:
|
|||
megalodon:
|
||||
specifier: workspace:*
|
||||
version: link:../megalodon
|
||||
meilisearch:
|
||||
specifier: 0.37.0
|
||||
version: 0.37.0
|
||||
mfm-js:
|
||||
specifier: 0.24.0
|
||||
version: 0.24.0
|
||||
|
@ -336,9 +330,6 @@ importers:
|
|||
sharp:
|
||||
specifier: 0.33.2
|
||||
version: 0.33.2
|
||||
sonic-channel:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
stringz:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
|
@ -1889,30 +1880,6 @@ packages:
|
|||
universalify: 0.1.2
|
||||
dev: false
|
||||
|
||||
/@elastic/elasticsearch@8.12.2:
|
||||
resolution: {integrity: sha512-04NvH3LIgcv1Uwguorfw2WwzC9Lhfsqs9f0L6uq6MrCw0lqe/HOQ6E8vJ6EkHAA15iEfbhtxOtenbZVVcE+mAQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
'@elastic/transport': 8.4.1
|
||||
tslib: 2.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@elastic/transport@8.4.1:
|
||||
resolution: {integrity: sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==}
|
||||
engines: {node: '>=16'}
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
hpagent: 1.2.0
|
||||
ms: 2.1.3
|
||||
secure-json-parse: 2.7.0
|
||||
tslib: 2.6.2
|
||||
undici: 5.23.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@emnapi/runtime@0.45.0:
|
||||
resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==}
|
||||
requiresBuild: true
|
||||
|
@ -7182,6 +7149,7 @@ packages:
|
|||
node-fetch: 2.6.12
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/cross-spawn@5.1.0:
|
||||
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
|
||||
|
@ -12529,14 +12497,6 @@ packages:
|
|||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/meilisearch@0.37.0:
|
||||
resolution: {integrity: sha512-LdbK6JmRghCawrmWKJSEQF0OiE82md+YqJGE/U2JcCD8ROwlhTx0KM6NX4rQt0u0VpV0QZVG9umYiu3CSSIJAQ==}
|
||||
dependencies:
|
||||
cross-fetch: 3.1.8
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/memoize@10.0.0:
|
||||
resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -15181,10 +15141,6 @@ packages:
|
|||
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||
dev: true
|
||||
|
||||
/secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
dev: false
|
||||
|
||||
/seedrandom@2.4.2:
|
||||
resolution: {integrity: sha512-uQ72txMoObtuJooiBLSVs5Yu2e9d/lHQz0boaqHjW8runXB9vR8nFtaZV54wYii613N0C8ZqTBLsfwDhAdpvqQ==}
|
||||
dev: false
|
||||
|
@ -15462,11 +15418,6 @@ packages:
|
|||
smart-buffer: 4.2.0
|
||||
dev: false
|
||||
|
||||
/sonic-channel@1.3.1:
|
||||
resolution: {integrity: sha512-+K4IZVFE7Tf2DB4EFZ23xo7a/+gJaiOHhFzXVZpzkX6Rs/rvf4YbSxnEGdYw8mrTcjtpG+jLVQEhP8sNTtN5VA==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
dev: false
|
||||
|
||||
/sort-keys-length@1.0.1:
|
||||
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
Loading…
Reference in a new issue