Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa5ee0a1d0 | |||
| d9930a9963 | |||
| 84539c5ac2 | |||
| 07ca7f6610 | |||
| 5d73f4bcb1 | |||
| 5f6d79fb1d | |||
| afb99e952e | |||
| 57c898c70c | |||
| 28f7e9ac97 | |||
| b6c178d243 | |||
| a5f1451d32 | |||
| 16bbeff1bf | |||
| 65856e18a4 | |||
| ca054327f7 | |||
| 2c0cebc7f8 | |||
| 503aa21cda | |||
| 97ec7b9b67 | |||
| c074d4468e | |||
| d5991ea0d4 | |||
| ab74459f36 | |||
| c892700e1c | |||
| ea96c54d23 | |||
| caf66ec3c6 | |||
| 994749c4e2 |
@@ -15,7 +15,6 @@ on:
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-mahjong/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
@@ -30,7 +29,6 @@ on:
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-mahjong/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
@@ -66,7 +64,6 @@ jobs:
|
||||
- sw
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-mahjong
|
||||
- misskey-reversi
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
|
||||
@@ -54,110 +54,55 @@ jobs:
|
||||
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
|
||||
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
|
||||
|
||||
variation() {
|
||||
calc() {
|
||||
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0')
|
||||
HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0')
|
||||
|
||||
DIFF=$((HEAD - BASE))
|
||||
if [ "$BASE" -gt 0 ]; then
|
||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
|
||||
else
|
||||
DIFF_PERCENT=0
|
||||
fi
|
||||
# Calculate difference
|
||||
if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then
|
||||
DIFF=$((HEAD_RSS - BASE_RSS))
|
||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
|
||||
|
||||
# Convert KB to MB for readability
|
||||
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
|
||||
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
|
||||
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
|
||||
# Convert to MB for readability
|
||||
BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
|
||||
HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
|
||||
DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson base "$BASE_MB" \
|
||||
--argjson head "$HEAD_MB" \
|
||||
--argjson diff "$DIFF_MB" \
|
||||
--argjson diff_percent "$DIFF_PERCENT" \
|
||||
'{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
|
||||
echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
|
||||
echo "has_data=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$JSON"
|
||||
}
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson VmRSS "$(calc $1 VmRSS)" \
|
||||
--argjson VmHWM "$(calc $1 VmHWM)" \
|
||||
--argjson VmSize "$(calc $1 VmSize)" \
|
||||
--argjson VmData "$(calc $1 VmData)" \
|
||||
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
|
||||
|
||||
echo "$JSON"
|
||||
}
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson beforeGc "$(variation beforeGc)" \
|
||||
--argjson afterGc "$(variation afterGc)" \
|
||||
--argjson afterRequest "$(variation afterRequest)" \
|
||||
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
|
||||
|
||||
echo "res=$JSON" >> "$GITHUB_OUTPUT"
|
||||
# Determine if this is a significant change (more than 5% increase)
|
||||
if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
|
||||
echo "significant_increase=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "significant_increase=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "has_data=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- id: build-comment
|
||||
name: Build memory comment
|
||||
env:
|
||||
RES: ${{ steps.compare.outputs.res }}
|
||||
run: |
|
||||
HEADER="## Backend memory usage comparison"
|
||||
HEADER="## Backend Memory Usage Comparison"
|
||||
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
||||
|
||||
echo "$HEADER" > ./output.md
|
||||
echo >> ./output.md
|
||||
|
||||
table() {
|
||||
echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
|
||||
echo "|--------|------:|------:|------:|------:|" >> ./output.md
|
||||
if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then
|
||||
echo "| Metric | base | head | Diff |" >> ./output.md
|
||||
echo "|--------|------|------|------|" >> ./output.md
|
||||
echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
|
||||
echo >> ./output.md
|
||||
|
||||
line() {
|
||||
METRIC=$2
|
||||
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
|
||||
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
|
||||
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
|
||||
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
|
||||
|
||||
if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
|
||||
DIFF="+$DIFF"
|
||||
DIFF_PERCENT="+$DIFF_PERCENT"
|
||||
fi
|
||||
|
||||
# highlight VmRSS
|
||||
if [ "$2" = "VmRSS" ]; then
|
||||
METRIC="**${METRIC}**"
|
||||
BASE="**${BASE}**"
|
||||
HEAD="**${HEAD}**"
|
||||
DIFF="**${DIFF}**"
|
||||
DIFF_PERCENT="**${DIFF_PERCENT}**"
|
||||
fi
|
||||
|
||||
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
|
||||
}
|
||||
|
||||
line $1 VmRSS
|
||||
line $1 VmHWM
|
||||
line $1 VmSize
|
||||
line $1 VmData
|
||||
}
|
||||
|
||||
echo "### Before GC" >> ./output.md
|
||||
table beforeGc
|
||||
echo >> ./output.md
|
||||
|
||||
echo "### After GC" >> ./output.md
|
||||
table afterGc
|
||||
echo >> ./output.md
|
||||
|
||||
echo "### After Request" >> ./output.md
|
||||
table afterRequest
|
||||
echo >> ./output.md
|
||||
|
||||
# Determine if this is a significant change (more than 5% increase)
|
||||
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
echo >> ./output.md
|
||||
fi
|
||||
else
|
||||
echo "Could not retrieve memory usage data." >> ./output.md
|
||||
echo >> ./output.md
|
||||
fi
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Build dependent packages
|
||||
run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-mahjong -F misskey-reversi build
|
||||
run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build
|
||||
- name: Build storybook
|
||||
run: pnpm --filter frontend build-storybook
|
||||
- name: Publish to Chromatic
|
||||
|
||||
@@ -30,7 +30,6 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-mahjong/package.json", "./packages/misskey-mahjong/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -62,7 +61,6 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-mahjong/package.json", "./packages/misskey-mahjong/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -99,12 +97,10 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-mahjong/node_modules ./packages/misskey-mahjong/node_modules
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-mahjong/built ./packages/misskey-mahjong/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Regenera"
|
||||
fontSize: "Mida del text"
|
||||
mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge"
|
||||
limitTo: "Limita a {x}"
|
||||
showMediaListByGridInWideArea: "Mostra la llista de medis en vista quadrícula quan l'amplada de la pantalla ho permeti"
|
||||
noFollowRequests: "No tens sol·licituds de seguiment"
|
||||
openImageInNewTab: "Obre imatges a una nova pestanya"
|
||||
dashboard: "Tauler de control"
|
||||
@@ -1389,7 +1390,7 @@ defaultCompressionLevel_description: "Si el redueixes augmentaràs la qualitat d
|
||||
inMinutes: "Minut(s)"
|
||||
inDays: "Di(a)(es)"
|
||||
safeModeEnabled: "Mode segur activat"
|
||||
pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mode segur està activat."
|
||||
pluginsAreDisabledBecauseSafeMode: "Les extensions no estan activades perquè el mode segur està activat."
|
||||
customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat."
|
||||
themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit."
|
||||
thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!"
|
||||
@@ -2184,8 +2185,8 @@ _email:
|
||||
title: "Has rebut una sol·licitud de seguiment"
|
||||
_plugin:
|
||||
install: "Instal·lar un afegit "
|
||||
installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
|
||||
manage: "Gestionar els afegits"
|
||||
installWarn: "Si us plau, no instal·lis extensions que no siguin de confiança."
|
||||
manage: "Gestiona les extensions"
|
||||
viewSource: "Veure l'origen "
|
||||
viewLog: "Mostra el registre"
|
||||
_preferencesBackups:
|
||||
|
||||
@@ -1406,6 +1406,7 @@ youAreAdmin: "Sie sind ein Administrator"
|
||||
frame: "Rahmen"
|
||||
presets: "Vorlage"
|
||||
zeroPadding: "Nullauffüllung"
|
||||
nothingToConfigure: "Es sind keine Einstellungen verfügbar"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Dateibeschriftung"
|
||||
@@ -1421,12 +1422,22 @@ _imageEditing:
|
||||
camera_lens_model: "Objektivname"
|
||||
camera_mm: "Brennweite"
|
||||
camera_mm_35: "Brennweite (35-mm-Äquivalent)"
|
||||
gps_lat: "Breitengrad"
|
||||
gps_long: "Längengrad"
|
||||
_imageFrameEditor:
|
||||
header: "Kopfzeile"
|
||||
footer: "Fußzeile"
|
||||
centered: "Zentriert"
|
||||
backgroundColor: "Hintergrundfarbe"
|
||||
textColor: "Textfarbe"
|
||||
font: "Schriftart"
|
||||
fontSerif: "Serif"
|
||||
fontSansSerif: "Sans Serif"
|
||||
quitWithoutSaveConfirm: "Nicht gespeicherte Änderungen verwerfen?"
|
||||
_compression:
|
||||
_quality:
|
||||
medium: "Mittlere Qualität"
|
||||
low: "Niedrige Qualität"
|
||||
_order:
|
||||
newest: "Neueste zuerst"
|
||||
oldest: "Älteste zuerst"
|
||||
@@ -1519,6 +1530,7 @@ _settings:
|
||||
contentsUpdateFrequency_description2: "Wenn der Echtzeitmodus aktiviert ist, werden die Inhalte unabhängig von dieser Einstellung in Echtzeit aktualisiert."
|
||||
showUrlPreview: "URL-Vorschau anzeigen"
|
||||
showAvailableReactionsFirstInNote: "Zeige die verfügbaren Reaktionen im oberen Bereich an."
|
||||
enableAnimatedImages: "Animierte Bilder aktivieren"
|
||||
_chat:
|
||||
showSenderName: "Name des Absenders anzeigen"
|
||||
sendOnEnter: "Eingabetaste sendet Nachricht"
|
||||
@@ -1710,6 +1722,7 @@ _serverSettings:
|
||||
userGeneratedContentsVisibilityForVisitor: "Sichtbarkeit von nutzergenerierten Inhalten für Gäste"
|
||||
userGeneratedContentsVisibilityForVisitor_description: "Dies ist nützlich, um zu verhindern, dass unangemessene Inhalte, die nicht gut moderiert sind, ungewollt über deinen eigenen Server im Internet veröffentlicht werden."
|
||||
userGeneratedContentsVisibilityForVisitor_description2: "Die uneingeschränkte Veröffentlichung aller Inhalte des Servers im Internet, einschließlich der vom Server empfangenen Fremdinhalte, birgt Risiken. Dies ist besonders wichtig für Betrachter, die sich des dezentralen Charakters der Inhalte nicht bewusst sind, da sie selbst fremde Inhalte fälschlicherweise als auf dem Server erstellte Inhalte wahrnehmen könnten."
|
||||
restartServerSetupWizardConfirm_text: "Einige aktuelle Einstellungen werden zurückgesetzt."
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Alles ist öffentlich"
|
||||
localOnly: "Nur lokale Inhalte werden veröffentlicht, fremde Inhalte bleiben privat"
|
||||
@@ -2318,6 +2331,7 @@ _time:
|
||||
minute: "Minute(n)"
|
||||
hour: "Stunde(n)"
|
||||
day: "Tag(en)"
|
||||
month: "Monat(e)"
|
||||
_2fa:
|
||||
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert."
|
||||
registerTOTP: "Authentifizierungs-App registrieren"
|
||||
@@ -2447,6 +2461,7 @@ _auth:
|
||||
scopeUser: "Als folgender Benutzer agieren"
|
||||
pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren."
|
||||
byClickingYouWillBeRedirectedToThisUrl: "Wenn der Zugang gewährt wird, wirst du automatisch zu folgender URL weitergeleitet"
|
||||
alreadyAuthorized: "Dieser Anwendung wurde bereits Zugriff gewährt."
|
||||
_antennaSources:
|
||||
all: "Alle Notizen"
|
||||
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
|
||||
@@ -2495,11 +2510,28 @@ _widgets:
|
||||
chat: "Mit dem Benutzer chatten"
|
||||
_widgetOptions:
|
||||
showHeader: "Kopfzeile anzeigen"
|
||||
transparent: "Hintergrund transparent machen"
|
||||
height: "Höhe"
|
||||
_button:
|
||||
colored: "Farbig"
|
||||
_clock:
|
||||
size: "Größe"
|
||||
thicknessThin: "Dünn"
|
||||
thicknessMedium: "Normal"
|
||||
thicknessThick: "Dick"
|
||||
twentyFour: "24-Stunden-Format"
|
||||
labelTime: "Uhrzeit"
|
||||
labelTz: "Zeitzone"
|
||||
timezone: "Zeitzone"
|
||||
showMs: "Millisekunden anzeigen"
|
||||
_jobQueue:
|
||||
sound: "Ton abspielen"
|
||||
_rss:
|
||||
url: "RSS-Feed-URL"
|
||||
refreshIntervalSec: "Aktualisierungsintervall (Sekunden)"
|
||||
maxEntries: "Maximale Anzahl der angezeigten Einträge"
|
||||
_rssTicker:
|
||||
shuffle: "Zufällige Anzeigereihenfolge"
|
||||
_birthdayFollowings:
|
||||
period: "Dauer"
|
||||
_cw:
|
||||
@@ -2694,6 +2726,8 @@ _notification:
|
||||
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
|
||||
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
|
||||
pollEnded: "Umfrageergebnisse sind verfügbar"
|
||||
scheduledNotePosted: "Geplante Notiz wurde veröffentlicht"
|
||||
scheduledNotePostFailed: "Veröffentlichen der geplanten Notiz fehlgeschlagen"
|
||||
newNote: "Neue Notiz"
|
||||
unreadAntennaNote: "Antenne {name}"
|
||||
roleAssigned: "Rolle zugewiesen"
|
||||
@@ -2762,6 +2796,9 @@ _deck:
|
||||
usedAsMinWidthWhenFlexible: "Ist \"Automatische Breitenanpassung\" aktiviert, wird hierfür die minimale Breite verwendet"
|
||||
flexible: "Automatische Breitenanpassung"
|
||||
enableSyncBetweenDevicesForProfiles: "Aktivieren der Synchronisierung von Profilinformationen zwischen Geräten"
|
||||
_howToUse:
|
||||
addColumn_title: "Spalte hinzufügen"
|
||||
settings_title: "UI-Einstellungen"
|
||||
_columns:
|
||||
main: "Hauptspalte"
|
||||
widgets: "Widgets"
|
||||
@@ -2822,6 +2859,8 @@ _abuseReport:
|
||||
notifiedWebhook: "Zu verwendender Webhook"
|
||||
deleteConfirm: "Bist du sicher, dass du den Empfänger der Benachrichtigung entfernen möchtest?"
|
||||
_moderationLogTypes:
|
||||
clearQueue: "Warteschlange leeren"
|
||||
promoteQueue: "Warteschlange erneut ausführen"
|
||||
createRole: "Rolle erstellt"
|
||||
deleteRole: "Rolle gelöscht"
|
||||
updateRole: "Rolle aktualisiert"
|
||||
@@ -3131,6 +3170,7 @@ _bootErrors:
|
||||
otherOption1: "Client-Einstellungen und Cache löschen"
|
||||
otherOption2: "Einfachen Client starten"
|
||||
otherOption3: "Starte das Reparaturwerkzeug"
|
||||
otherOption4: "Misskey im abgesicherten Modus starten"
|
||||
_search:
|
||||
searchScopeAll: "Alle"
|
||||
searchScopeLocal: "Lokal"
|
||||
@@ -3227,16 +3267,20 @@ _watermarkEditor:
|
||||
polkadotSubDotOpacity: "Deckkraft des Unterpunktes"
|
||||
polkadotSubDotRadius: "Größe des Unterpunktes"
|
||||
polkadotSubDotDivisions: "Anzahl der Unterpunkte"
|
||||
failedToLoadImage: "Bild konnte nicht geladen werden"
|
||||
_imageEffector:
|
||||
title: "Effekte"
|
||||
addEffect: "Effekte hinzufügen"
|
||||
discardChangesConfirm: "Änderungen verwerfen und beenden?"
|
||||
failedToLoadImage: "Bild konnte nicht geladen werden"
|
||||
_fxs:
|
||||
chromaticAberration: "Chromatische Abweichung"
|
||||
glitch: "Glitch"
|
||||
mirror: "Spiegeln"
|
||||
invert: "Farben umkehren"
|
||||
grayscale: "Schwarzweiß"
|
||||
blur: "Verwischen"
|
||||
pixelate: "Verpixeln"
|
||||
colorAdjust: "Farbkorrektur"
|
||||
colorClamp: "Farbkomprimierung"
|
||||
colorClampAdvanced: "Farbkomprimierung (erweitert)"
|
||||
@@ -3251,6 +3295,11 @@ _imageEffector:
|
||||
color: "Farbe"
|
||||
opacity: "Transparenz"
|
||||
lightness: "Erhellen"
|
||||
saturation: "Sättigung"
|
||||
max: "Maximum"
|
||||
min: "Minimum"
|
||||
direction: "Richtung"
|
||||
frequency: "Häufigkeit"
|
||||
drafts: "Entwurf"
|
||||
_drafts:
|
||||
select: "Entwurf auswählen"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Regenerate"
|
||||
fontSize: "Font size"
|
||||
mediaListWithOneImageAppearance: "Height of media lists with one image only"
|
||||
limitTo: "Limit to {x}"
|
||||
showMediaListByGridInWideArea: "Display the media list in a grid when the screen width is wide"
|
||||
noFollowRequests: "You don't have any pending follow requests"
|
||||
openImageInNewTab: "Open images in new tab"
|
||||
dashboard: "Dashboard"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Regenerar"
|
||||
fontSize: "Tamaño de la letra"
|
||||
mediaListWithOneImageAppearance: "Altura de la lista de medios con una sola imagen."
|
||||
limitTo: "{x} hasta un máximo de"
|
||||
showMediaListByGridInWideArea: "Cuando el ancho de la pantalla sea grande, muestra la lista de multimedia uno al lado del otro."
|
||||
noFollowRequests: "No hay solicitudes de seguimiento"
|
||||
openImageInNewTab: "Abrir imagen en nueva pestaña"
|
||||
dashboard: "Panel de control"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Generare di nuovo"
|
||||
fontSize: "Dimensione carattere"
|
||||
mediaListWithOneImageAppearance: "Altezza dell'elenco media con una sola immagine "
|
||||
limitTo: "Limita a {x}"
|
||||
showMediaListByGridInWideArea: "Quando la larghezza dello schermo è ampia, mostra i media affiancati"
|
||||
noFollowRequests: "Non ci sono richieste di relazione"
|
||||
openImageInNewTab: "Apri le immagini in un nuovo tab"
|
||||
dashboard: "Pannello di controllo"
|
||||
@@ -556,7 +557,7 @@ clientSettings: "Impostazioni client"
|
||||
accountSettings: "Impostazioni profilo"
|
||||
promotion: "Promossa"
|
||||
promote: "Pubblicizza"
|
||||
numberOfDays: "Numero di giorni"
|
||||
numberOfDays: ""
|
||||
hideThisNote: "Nasconda la nota"
|
||||
showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline"
|
||||
objectStorage: "Storage S3"
|
||||
@@ -701,7 +702,7 @@ hardWordMuteDescription: "Ignora le Note con la parola o la regola specificata.
|
||||
regexpError: "errore regex"
|
||||
regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
|
||||
instanceMute: "Silenziare l'istanza"
|
||||
userSaysSomething: "{name} ha detto qualcosa"
|
||||
userSaysSomething: "{name} ha scritto qualcosa"
|
||||
userSaysSomethingAbout: "{name} ha anNotato qualcosa su \"{word}\""
|
||||
makeActive: "Attiva"
|
||||
display: "Visualizza"
|
||||
@@ -2070,7 +2071,7 @@ _role:
|
||||
gtlAvailable: "Disponibilità della Timeline Federata"
|
||||
ltlAvailable: "Disponibilità della Timeline Locale"
|
||||
canPublicNote: "Scrivere Note con Visibilità Pubblica"
|
||||
mentionMax: "Numero massimo di menzioni in una nota"
|
||||
mentionMax: ""
|
||||
canInvite: "Generare codici di invito all'istanza"
|
||||
inviteLimit: "Limite di codici invito"
|
||||
inviteLimitCycle: "Intervallo di emissione del codice di invito"
|
||||
@@ -2436,7 +2437,7 @@ _permissions:
|
||||
"read:flash-likes": "Visualizza lista di Play piaciuti"
|
||||
"write:flash-likes": "Modifica lista di Play piaciuti"
|
||||
"read:admin:abuse-user-reports": "Mostra i report dai profili utente"
|
||||
"write:admin:delete-account": "Elimina l'account utente"
|
||||
"write:admin:delete-account": "Elimina l'utenza"
|
||||
"write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente"
|
||||
"read:admin:index-stats": "Visualizza informazioni sugli indici del database"
|
||||
"read:admin:table-stats": "Visualizza informazioni sulle tabelle del database"
|
||||
@@ -2532,19 +2533,19 @@ _widgets:
|
||||
instanceCloud: "Nuvola di federazione"
|
||||
postForm: "Finestra di pubblicazione"
|
||||
slideshow: "Diapositive"
|
||||
button: "Pulsante"
|
||||
button: "Bottone"
|
||||
onlineUsers: "Persone attive adesso"
|
||||
jobQueue: "Coda di lavoro"
|
||||
serverMetric: "Statistiche server"
|
||||
aiscript: "Console AiScript"
|
||||
aiscriptApp: "App AiScript"
|
||||
aichan: "Mascotte Ai"
|
||||
userList: "Elenco utenti"
|
||||
userList: "Lista profili"
|
||||
_userList:
|
||||
chooseList: "Seleziona una lista"
|
||||
clicker: "Cliccheria"
|
||||
birthdayFollowings: "Compleanni del giorno"
|
||||
chat: "Chatta con questa persona"
|
||||
chat: "Messaggi diretti"
|
||||
_widgetOptions:
|
||||
showHeader: "Mostra la testata"
|
||||
transparent: "Sfondo trasparente"
|
||||
@@ -2660,7 +2661,7 @@ _profile:
|
||||
metadataContent: "Contenuto"
|
||||
changeAvatar: "Modifica immagine profilo"
|
||||
changeBanner: "Cambia intestazione"
|
||||
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo.\nPer verificare il profilo tramite la spunta di conferma, devi inserire la url alla pagina che contiene un link al tuo profilo Misskey. Deve avere attributo rel='me'."
|
||||
verifiedLinkDescription: "Come avere i collegamenti verificati: inserisci la URL ad una pagina che contiene un collegamento al tuo profilo.\nVedrai una spunta di conferma se, in quella pagina, il collegamento al tuo profilo Misskey ha attributo rel='me'."
|
||||
avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
|
||||
followedMessage: "Messaggio, quando qualcuno ti segue"
|
||||
followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
|
||||
@@ -2780,7 +2781,7 @@ _notification:
|
||||
fileUploaded: "File caricato correttamente"
|
||||
youGotMention: "{name} ti ha menzionato"
|
||||
youGotReply: "{name} ti ha risposto"
|
||||
youGotQuote: "{name} ha citato la tua Nota e ha detto"
|
||||
youGotQuote: "{name} ha scritto citando la tua Nota"
|
||||
youRenoted: "{name} ha rinotato"
|
||||
youWereFollowed: "Follower aggiuntivo"
|
||||
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
|
||||
|
||||
@@ -3180,79 +3180,6 @@ _reversi:
|
||||
showBoardLabels: "盤面に行・列番号を表示"
|
||||
useAvatarAsStone: "石をアイコンにする"
|
||||
|
||||
_mahjong:
|
||||
mahjong: "麻雀"
|
||||
joinRoom: "ルームに参加"
|
||||
createRoom: "ルームを作成"
|
||||
ready: "準備完了"
|
||||
cancelReady: "準備を再開"
|
||||
leave: "退室"
|
||||
addCpu: "CPUを追加"
|
||||
east: "東"
|
||||
south: "南"
|
||||
west: "西"
|
||||
north: "北"
|
||||
dora: "ドラ"
|
||||
redDora: "赤ドラ"
|
||||
fan: "飜"
|
||||
_fanNames:
|
||||
mangan: "満貫"
|
||||
haneman: "跳満"
|
||||
baiman: "倍満"
|
||||
sanbaiman: "三倍満"
|
||||
yakuman: "役満"
|
||||
kazoeyakuman: "数え役満"
|
||||
_yakus:
|
||||
"riichi": "立直"
|
||||
"ippatsu": "一発"
|
||||
"tsumo": "門前清自摸和"
|
||||
"tanyao": "断么"
|
||||
"pinfu": "平和"
|
||||
"iipeko": "一盃口"
|
||||
"field-wind-e": "東"
|
||||
"field-wind-s": "南"
|
||||
"seat-wind-e": "東"
|
||||
"seat-wind-s": "南"
|
||||
"seat-wind-w": "西"
|
||||
"seat-wind-n": "北"
|
||||
"white": "白"
|
||||
"green": "發"
|
||||
"red": "中"
|
||||
"rinshan": "嶺上開花"
|
||||
"chankan": "搶槓"
|
||||
"haitei": "海底摸月"
|
||||
"hotei": "河底撈魚"
|
||||
"sanshoku-dojun": "三色同順"
|
||||
"sanshoku-doko": "三色同刻"
|
||||
"ittsu": "一気通貫"
|
||||
"chanta": "混全帯么九"
|
||||
"chitoitsu": "七対子"
|
||||
"toitoi": "対々"
|
||||
"sananko": "三暗刻"
|
||||
"honroto": "混老頭"
|
||||
"sankantsu": "三槓子"
|
||||
"shosangen": "小三元"
|
||||
"double-riichi": "ダブル立直"
|
||||
"honitsu": "混一色"
|
||||
"junchan": "清全帯么九"
|
||||
"ryampeko": "ニ盃口"
|
||||
"chinitsu": "清一色"
|
||||
"kokushi": "国士無双"
|
||||
"kokushi-13": "国士無双十三面待"
|
||||
"suanko": "四暗刻"
|
||||
"suanko-tanki": "四暗刻単騎待"
|
||||
"daisangen": "大三元"
|
||||
"tsuiso": "字一色"
|
||||
"shosushi": "小四喜"
|
||||
"daisushi": "大四喜"
|
||||
"ryuiso": "緑一色"
|
||||
"chinroto": "清老頭"
|
||||
"sukantsu": "四槓子"
|
||||
"churen": "九蓮宝燈"
|
||||
"churen-9": "九連宝灯九面待"
|
||||
"tenho": "天和"
|
||||
"chiho": "地和"
|
||||
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できません"
|
||||
header: "サーバーに接続できません"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "재생성"
|
||||
fontSize: "글자 크기"
|
||||
mediaListWithOneImageAppearance: "이미지가 1개 뿐인 미디어 목록의 높이"
|
||||
limitTo: "{x}로 제한"
|
||||
showMediaListByGridInWideArea: "화면 폭이 넓을 때는 미디어 목록을 가로로 표시하기"
|
||||
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
|
||||
openImageInNewTab: "새 탭에서 이미지 열기"
|
||||
dashboard: "대시보드"
|
||||
@@ -1334,7 +1335,7 @@ markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠
|
||||
unmarkAsSensitiveConfirm: "이 미디어의 민감한 미디어 지정을 해제하시겠습니까?"
|
||||
preferences: "환경설정"
|
||||
accessibility: "접근성"
|
||||
preferencesProfile: "설정 프로필"
|
||||
preferencesProfile: "설정 프로파일"
|
||||
copyPreferenceId: "설정한 ID를 복사"
|
||||
resetToDefaultValue: "기본값으로 되돌리기"
|
||||
overrideByAccount: "계정으로 덮어쓰기"
|
||||
@@ -1347,7 +1348,7 @@ preferenceSyncConflictTitle: "서버에 설정값이 존재합니다."
|
||||
preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?"
|
||||
preferenceSyncConflictChoiceMerge: "병합"
|
||||
preferenceSyncConflictChoiceServer: "서버 설정값"
|
||||
preferenceSyncConflictChoiceDevice: "장치 설정값"
|
||||
preferenceSyncConflictChoiceDevice: "장치 설정 값"
|
||||
preferenceSyncConflictChoiceCancel: "동기화 취소"
|
||||
paste: "붙여넣기"
|
||||
emojiPalette: "이모지 팔레트"
|
||||
@@ -1558,11 +1559,11 @@ _settings:
|
||||
showSenderName: "발신자 이름 표시"
|
||||
sendOnEnter: "엔터로 보내기"
|
||||
_preferencesProfile:
|
||||
profileName: "프로필 이름"
|
||||
profileName: "프로파일 이름"
|
||||
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
|
||||
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
|
||||
manageProfiles: "프로파일 관리"
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "여러 장치에서 동일한 프로필을 공유하는 것은 권장하지 않습니다."
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "여러 장치에서 같은 프로파일을 공유하는 것은 권장하지 않습니다."
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "여러 장치에서 동기화하고 싶은 설정 항목이 있는 경우에는 개별로 '여러 장치에서 동기화' 옵션을 활성화해 주십시오."
|
||||
_preferencesBackup:
|
||||
autoBackup: "자동 백업"
|
||||
@@ -1570,7 +1571,7 @@ _preferencesBackup:
|
||||
noBackupsFoundTitle: "백업을 찾을 수 없습니다"
|
||||
noBackupsFoundDescription: "자동으로 생성된 백업은 찾을 수 없었지만, 수동으로 백업 파일을 저장한 경우 해당 파일을 가져와 복원할 수 있습니다."
|
||||
selectBackupToRestore: "복원할 백업을 선택하세요"
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로필 이름을 설정해야 합니다."
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로파일 이름을 설정해야 합니다."
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "이 장치에서 설정 자동 백업이 활성화되어 있지 않습니다."
|
||||
backupFound: "설정 백업이 발견되었습니다"
|
||||
forceBackup: "설정 강제 백업"
|
||||
@@ -2857,15 +2858,15 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
|
||||
usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다"
|
||||
flexible: "폭 자동 조정"
|
||||
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화"
|
||||
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 장치 간 동기화를 활성화"
|
||||
showHowToUse: "UI 설명 보기"
|
||||
_howToUse:
|
||||
addColumn_title: "칼럼 추가"
|
||||
addColumn_description: "칼럼의 종류를 선택해 추가할 수 있습니다."
|
||||
settings_title: "UI 설정"
|
||||
settings_description: "덱 UI의 상세 설정을 할 수 있습니다."
|
||||
switchProfile_title: "프로필 전환"
|
||||
switchProfile_description: "UI의 레이아웃을 프로필로 저장하고, 언제나 전환할 수 있습니다."
|
||||
switchProfile_title: "프로파일 전환"
|
||||
switchProfile_description: "UI의 레이아웃을 프로파일로 저장하고 언제든지 전환할 수 있습니다."
|
||||
_columns:
|
||||
main: "메인"
|
||||
widgets: "위젯"
|
||||
|
||||
@@ -1231,6 +1231,7 @@ showRepliesToOthersInTimelineAll: "Показывать в ленте ответ
|
||||
hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны"
|
||||
confirmShowRepliesAll: "Это нельзя будет отменить. Показать ответы от всех, на кого вы подписаны?"
|
||||
confirmHideRepliesAll: "Это нельзя будет отменить. Скрыть ответы от всех, на кого вы подписаны?"
|
||||
externalServices: "Интеграции"
|
||||
sourceCode: "Исходный код"
|
||||
sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему."
|
||||
repositoryUrl: "Ссылка на репозиторий"
|
||||
|
||||
@@ -83,6 +83,8 @@ files: "ไฟล์"
|
||||
download: "ดาวน์โหลด"
|
||||
driveFileDeleteConfirm: "ต้องการลบไฟล์ “{name}” ใช่ไหม? โน้ตที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย"
|
||||
unfollowConfirm: "ต้องการเลิกติดตาม {name} ใช่ไหม?"
|
||||
cancelFollowRequestConfirm: "ยกเลิกคำขอติดตาม {name} ใช่ไหม?"
|
||||
rejectFollowRequestConfirm: "ปฏิเสธคำขอติดตามจาก {name} ใช่ไหม?"
|
||||
exportRequested: "คุณได้ร้องขอการส่งออก อาจใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว"
|
||||
importRequested: "คุณได้ร้องขอการนำเข้า การดำเนินการนี้อาจใช้เวลาสักครู่"
|
||||
lists: "รายชื่อ"
|
||||
@@ -204,7 +206,7 @@ host: "โฮสต์"
|
||||
selectSelf: "เลือกตัวเอง"
|
||||
selectUser: "เลือกผู้ใช้งาน"
|
||||
recipient: "ผู้รับ"
|
||||
annotation: "หมายเหตุประกอบ"
|
||||
annotation: "ข้อความเกริ่น"
|
||||
federation: "สหพันธ์"
|
||||
instances: "เซิร์ฟเวอร์"
|
||||
registeredAt: "วันที่ลงทะเบียน"
|
||||
@@ -222,7 +224,7 @@ operations: "ดำเนินการ"
|
||||
software: "ซอฟต์แวร์"
|
||||
softwareName: "ชื่อซอฟต์แวร์"
|
||||
version: "เวอร์ชั่น"
|
||||
metadata: "Metadata"
|
||||
metadata: "เมทาเดต้า"
|
||||
withNFiles: "{n} ไฟล์"
|
||||
monitor: "มอนิเตอร์"
|
||||
jobQueue: "คิวงาน"
|
||||
@@ -302,6 +304,7 @@ uploadFromUrlMayTakeTime: "การอัปโหลดอาจใช้เ
|
||||
uploadNFiles: "อัปโหลด {n} ไฟล์"
|
||||
explore: "สำรวจ"
|
||||
messageRead: "อ่านแล้ว"
|
||||
readAllChatMessages: "ทำเครื่องหมายใส่ข้อความทั้งหมดว่าอ่านแล้ว"
|
||||
noMoreHistory: "ไม่มีประวัติเพิ่มเติม"
|
||||
startChat: "เริ่มแชต"
|
||||
nUsersRead: "อ่านโดย {n}"
|
||||
@@ -334,6 +337,7 @@ fileName: "ชื่อไฟล์"
|
||||
selectFile: "เลือกไฟล์"
|
||||
selectFiles: "เลือกไฟล์"
|
||||
selectFolder: "เลือกโฟลเดอร์"
|
||||
unselectFolder: "ยกเลิกการเลือกโฟลเดอร์"
|
||||
selectFolders: "เลือกโฟลเดอร์"
|
||||
fileNotSelected: "ยังไม่ได้เลือกไฟล์"
|
||||
renameFile: "เปลี่ยนชื่อไฟล์"
|
||||
@@ -346,6 +350,7 @@ addFile: "เพิ่มไฟล์"
|
||||
showFile: "แสดงไฟล์"
|
||||
emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ"
|
||||
emptyFolder: "โฟลเดอร์นี้ว่างเปล่า"
|
||||
dropHereToUpload: "ดรอปไฟล์ลงที่นี่เพื่ออัปโหลด"
|
||||
unableToDelete: "ไม่สามารถลบออกได้"
|
||||
inputNewFileName: "ป้อนชื่อไฟล์ใหม่"
|
||||
inputNewDescription: "กรุณาใส่แคปชั่นใหม่"
|
||||
@@ -428,7 +433,7 @@ antennaKeywordsDescription: "คั่นด้วยเว้นวรรคส
|
||||
notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
||||
withFileAntenna: "เฉพาะโน้ตที่มีไฟล์"
|
||||
excludeNotesInSensitiveChannel: "ไม่รวมโน้ตจากช่องเนื้อหาละเอียดอ่อน"
|
||||
enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ"
|
||||
enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์"
|
||||
antennaUsersDescription: "ระบุหนึ่งชื่อผู้ใช้ต่อบรรทัด"
|
||||
caseSensitive: "อักษรพิมพ์ใหญ่-พิมพ์เล็กความหมายต่างกัน"
|
||||
withReplies: "รวมตอบกลับ"
|
||||
@@ -538,6 +543,7 @@ regenerate: "สร้างอีกครั้ง"
|
||||
fontSize: "ขนาดตัวอักษร"
|
||||
mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว"
|
||||
limitTo: "จำกัดไว้ที่ {x}"
|
||||
showMediaListByGridInWideArea: "เมื่อหน้าจอกว้างยาวขึ้น ให้เรียงรายการสื่อเป็นแนวนอน"
|
||||
noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ"
|
||||
openImageInNewTab: "เปิดรูปภาพในแท็บใหม่"
|
||||
dashboard: "หน้ากระดานหลัก"
|
||||
@@ -612,7 +618,7 @@ uiInspectorDescription: "คุณสามารถตรวจสอบรา
|
||||
output: "เอาท์พุต"
|
||||
script: "สคริปต์"
|
||||
disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ"
|
||||
updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล"
|
||||
updateRemoteUser: "อัปเดตข้อมูลผู้ใช้ระยะไกล"
|
||||
unsetUserAvatar: "เลิกตั้งไอคอน"
|
||||
unsetUserAvatarConfirm: "ต้องการเลิกตั้งไอคอนประจำตัวหรือไม่?"
|
||||
unsetUserBanner: "เลิกตั้งแบนเนอร์"
|
||||
@@ -773,6 +779,7 @@ lockedAccountInfo: "แม้ว่าการอนุมัติการต
|
||||
alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนเป็นค่าเริ่มต้น"
|
||||
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
||||
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
||||
disableShowingAnimatedImages_caption: "หากภาพเคลื่อนไหวไม่เล่นแม่จะปิดตั้งค่านี้ไปแล้ว อาจเป็นกรณีที่การตั้งค่าการช่วยการเข้าถึงหรือการประหยัดพลังงาน ของเบราว์เซอร์/OS เข้าแทรกแซง"
|
||||
highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน"
|
||||
verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น"
|
||||
notSet: "ไม่ได้ตั้งค่า"
|
||||
@@ -887,7 +894,7 @@ low: "ต่ำ"
|
||||
emailNotConfiguredWarning: "ยังไม่ได้ตั้งค่าที่อยู่อีเมล"
|
||||
ratio: "อัตราส่วน"
|
||||
previewNoteText: "แสดงตัวอย่าง"
|
||||
customCss: "CSS ที่กำหนดเอง"
|
||||
customCss: "CSS แบบกำหนดเอง"
|
||||
customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้มันใช้ทำอะไร การตั้งค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์ไม่สามารถใช้งานได้อย่างถูกต้อง"
|
||||
global: "ทั่วโลก"
|
||||
squareAvatars: "แสดงไอคอนประจำตัวเป็นสี่เหลี่ยม"
|
||||
@@ -930,7 +937,7 @@ unmuteThread: "เลิกปิดเสียงเธรด"
|
||||
followingVisibility: "การมองเห็นที่เรากำลังติดตาม"
|
||||
followersVisibility: "การมองเห็นผู้ที่กำลังติดตามเรา"
|
||||
continueThread: "ดูความต่อเนื่องเธรด"
|
||||
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
|
||||
deleteAccountConfirm: "บัญชีจะถูกลบ ดำเนินการต่อใช่ไหม?"
|
||||
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
|
||||
incorrectTotp: "รหัสยืนยันตัวตนแบบใช้ครั้งเดียวที่ท่านได้ระบุมานั้น ไม่ถูกต้องหรือหมดอายุลงแล้วค่ะ"
|
||||
voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?"
|
||||
@@ -991,7 +998,7 @@ pleaseSelect: "ตัวเลือก"
|
||||
reverse: "พลิก"
|
||||
colored: "สี"
|
||||
refreshInterval: "ความถี่ในการอัปเดต"
|
||||
label: "ป้ายชื่อ"
|
||||
label: "ป้าย"
|
||||
type: "รูปแบบ"
|
||||
speed: "ความเร็ว"
|
||||
slow: "ช้า"
|
||||
@@ -1019,6 +1026,9 @@ pushNotificationAlreadySubscribed: "การแจ้งเตือนแบ
|
||||
pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช"
|
||||
sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
|
||||
sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น"
|
||||
pleaseAllowPushNotification: "โปรดอนุญาตการตั้งค่าการแจ้งเตือนของเบราว์เซอร์"
|
||||
browserPushNotificationDisabled: "ขอสิทธิ์ส่งการแจ้งเตือนล้มเหลว"
|
||||
browserPushNotificationDisabledDescription: "ไม่มีสิทธิ์ในการส่งการแจ้งเตือนจาก {serverName} โปรดอนุญาตการแจ้งเตือนในตั้งค่าของเบราว์เซอร์ แล้วลองอีกครั้ง"
|
||||
windowMaximize: "ขยายใหญ่สุด"
|
||||
windowMinimize: "ย่อเล็กที่สุด"
|
||||
windowRestore: "เลิกทำ"
|
||||
@@ -1099,8 +1109,8 @@ license: "ใบอนุญาต"
|
||||
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||
myClips: "คลิปของฉัน"
|
||||
drivecleaner: "ทำความสะอาดไดรฟ์"
|
||||
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||
retryAllQueuesNow: "ลองใหม่ทุกคิวทันที"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทันทีเลยไหม?"
|
||||
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
||||
enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล"
|
||||
@@ -1150,7 +1160,7 @@ initialAccountSetting: "ตั้งค่าโปรไฟล์"
|
||||
youFollowing: "ติดตามแล้ว"
|
||||
preventAiLearning: "ปฏิเสธการเรียนรู้ด้วย generative AI"
|
||||
preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
options: "ตัวเลือก"
|
||||
specifyUser: "ผู้ใช้เฉพาะ"
|
||||
lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
|
||||
openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
|
||||
@@ -1169,6 +1179,7 @@ installed: "ติดตั้งแล้ว"
|
||||
branding: "แบรนดิ้ง"
|
||||
enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์"
|
||||
enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ"
|
||||
showRoleBadgesOfRemoteUsers: "แสดงตราบทบาทที่มอบให้กับผู้ใช้ระยะไกล"
|
||||
turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้"
|
||||
createInviteCode: "สร้างรหัสเชิญ"
|
||||
createWithOptions: "สร้างด้วยตัวเลือก"
|
||||
@@ -1247,7 +1258,7 @@ refreshing: "กำลังรีเฟรช..."
|
||||
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
|
||||
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
|
||||
emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว"
|
||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
|
||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุข้อความเกริ่น"
|
||||
doReaction: "เพิ่มรีแอคชั่น"
|
||||
code: "โค้ด"
|
||||
reloadRequiredToApplySettings: "จำเป็นต้องมีการโหลดซ้ำเพื่อให้การตั้งค่ามีผล"
|
||||
@@ -1390,17 +1401,53 @@ scheduledToPostOnX: "มีการกำหนดเวลาให้โพ
|
||||
schedule: "กำหนดเวลา"
|
||||
scheduled: "กำหนดเวลา"
|
||||
widgets: "วิดเจ็ต"
|
||||
deviceInfo: "รายละเอียดอุปกรณ์"
|
||||
deviceInfoDescription: "เมื่อต้องการรับความช่วยเหลือทางเทคนิค กรุณาระบุข้อมูลต่อไปนี้ซึ่งอาจช่วยแก้ไขปัญหาได้"
|
||||
youAreAdmin: "คุณคือผู้ดูแลระบบ"
|
||||
frame: "เฟรม"
|
||||
presets: "พรีเซ็ต"
|
||||
zeroPadding: "ห่างเป็น 0"
|
||||
nothingToConfigure: "ไม่มีอะไรให้ต้ังค่า"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "แคปชั่นของไฟล์"
|
||||
filename: "ชื่อไฟล์"
|
||||
filename_without_ext: "ชื่อไฟล์ที่ไม่มีนามสกุล"
|
||||
year: "ปีที่ถ่าย"
|
||||
month: "เดือนที่ถ่าย"
|
||||
day: "วันที่ถ่าย"
|
||||
hour: "เวลาที่ถ่าย (ชั่วโมง)"
|
||||
minute: "เวลาที่ถ่าย (นาที)"
|
||||
second: "เวลาที่ถ่าย (วินาที)"
|
||||
camera_model: "ชื่อกล้อง"
|
||||
camera_lens_model: "ชื่อเลนส์"
|
||||
camera_mm: "ความยาวโฟกัส"
|
||||
camera_mm_35: "ทางยาวโฟกัส (เทียบเท่า 35 มม.)"
|
||||
camera_f: "รูรับแสง"
|
||||
camera_s: "ความเร็วชัตเตอร์"
|
||||
camera_iso: "ความไวแสง ISO"
|
||||
gps_lat: "ละติจูด"
|
||||
gps_long: "ลองจิจูด"
|
||||
_imageFrameEditor:
|
||||
title: "แก้ไขเฟรม"
|
||||
tip: "สามารถตกแต่งภาพโดยการเพิ่มป้ายที่มีเฟรมหรือเมทาเดต้าได้"
|
||||
header: "ส่วนหัว"
|
||||
footer: "ท้ายกระดาษ"
|
||||
borderThickness: "ความกว้างขอบ"
|
||||
labelThickness: "ความกว้างป้าย"
|
||||
labelScale: "สเกลของป้าย"
|
||||
centered: "จัดกึ่งกลาง"
|
||||
captionMain: "แคปชั่น (ใหญ่)"
|
||||
captionSub: "แคปชั่น (เล็ก)"
|
||||
availableVariables: "ตัวแปรที่สามารถใช้ได้"
|
||||
withQrCode: "QR โค้ด"
|
||||
backgroundColor: "สีพื้นหลัง"
|
||||
textColor: "สีตัวอักษร"
|
||||
font: "แบบอักษร"
|
||||
fontSerif: "Serif"
|
||||
fontSansSerif: "Sans Serif"
|
||||
quitWithoutSaveConfirm: "ต้องการออกโดยไม่บันทึกหรือไม่?"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "คุณภาพสูง"
|
||||
@@ -1503,6 +1550,11 @@ _settings:
|
||||
showUrlPreview: "แสดงตัวอย่าง URL"
|
||||
showAvailableReactionsFirstInNote: "แสดงรีแอคชั่นที่ใช้ได้ไว้หน้าสุด"
|
||||
showPageTabBarBottom: "แสดงแท็บบาร์ของเพจที่ด้านล่าง"
|
||||
emojiPaletteBanner: "สามารถบันทึกพรีเซ็ตเป็นจานสีเพื่อตรึงไว้ในตัวจิ้มเอโมจิ หรือปรับแต่งวิธีการแสดงผลของตัวจิ้มเอโมจิได้"
|
||||
enableAnimatedImages: "เปิดใช้งานภาพเคลื่อนไหว"
|
||||
settingsPersistence_title: "คงสภาพการตั้งค่า"
|
||||
settingsPersistence_description1: "เมื่อเปิดใช้งานการคงสภาพการตั้งค่า จะช่วยป้องกันไม่ให้ข้อมูลการตั้งค่าสูญหายได้"
|
||||
settingsPersistence_description2: "แต่ในบางสภาพแวดล้อม อาจไม่สามารถเปิดใช้งานได้"
|
||||
_chat:
|
||||
showSenderName: "แสดงชื่อผู้ส่ง"
|
||||
sendOnEnter: "กด Enter เพื่อส่ง"
|
||||
@@ -1511,6 +1563,8 @@ _preferencesProfile:
|
||||
profileNameDescription: "กรุณาตั้งชื่อเพื่อระบุอุปกรณ์นี้"
|
||||
profileNameDescription2: "เช่น: “คอมเครื่องหลัก”, “มือถือ” ฯลฯ"
|
||||
manageProfiles: "จัดการโปรไฟล์"
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "ไม่แนะนำให้ใช้โปรไฟล์เดียวกันร่วมกันบนหลายอุปกรณ์"
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "หากมีรายการตั้งค่าที่ต้องการซิงก์ระหว่างหลายอุปกรณ์ โปรดเปิดใช้งานตัวเลือก “ซิงก์ระหว่างหลายอุปกรณ์” ในอุปกรณ์นั้นๆ ด้วย"
|
||||
_preferencesBackup:
|
||||
autoBackup: "สำรองโดยอัตโนมัติ"
|
||||
restoreFromBackup: "คืนค่าจากข้อมูลสำรอง"
|
||||
@@ -1518,8 +1572,9 @@ _preferencesBackup:
|
||||
noBackupsFoundDescription: "ไม่พบข้อมูลสำรองที่สร้างโดยอัตโนมัติ แต่หากมีข้อมูลสำรองที่บันทึกด้วยตนเอง สามารถนำเข้ามาเพื่อกู้คืนได้"
|
||||
selectBackupToRestore: "กรุณาเลือกข้อมูลสำรองที่ต้องการกู้คืน"
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "จำเป็นต้องตั้งชื่อโปรไฟล์ก่อนจึงจะเปิดใช้งานการสำรองข้อมูลอัตโนมัติได้"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "ยังไม่ได้เปิดใช้งานการสำรองข้อมูลอัตโนมัติบนอุปกรณ์นี้"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "ยังไม่ได้เปิดใช้งานการสำรองการตั้งค่าแบบอัตโนมัติบนอุปกรณ์นี้"
|
||||
backupFound: "พบข้อมูลสำรองของการตั้งค่าแล้ว"
|
||||
forceBackup: "บังคับสำรองการตั้งค่า"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
||||
requireSigninToViewContentsDescription1: "กำหนดให้ต้องเข้าสู่ระบบก่อนจึงจะสามารถดูโน้ตหรือเนื้อหาทั้งหมดที่สร้างไว้ได้ ซึ่งช่วยป้องกันไม่ให้ข้อมูลถูกเก็บโดยบอตหรือ Crawler (โปรแกรมรวบรวมข้อมูล)"
|
||||
@@ -1587,7 +1642,7 @@ _initialAccountSetting:
|
||||
theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
|
||||
youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “การตั้งค่า” อย่าลืมไปเยี่ยมชมภายหลังด้วย"
|
||||
followUsers: "ลองติดตามผู้ใช้ที่สนใจเพื่อสร้างไทม์ไลน์ดูสิ"
|
||||
pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
|
||||
pushNotificationDescription: "เมื่อเปิดใช้งานการแจ้งเตือนแบบพุช จะสามารถรับการแจ้งเตือนจาก {name} บนอุปกรณ์ที่ใช้งานอยู่ได้"
|
||||
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
|
||||
haveFun: "ขอให้สนุกกับ {name}!"
|
||||
youCanContinueTutorial: "คุณสามารถดำเนินการต่อด้วยบทช่วยสอนเกี่ยวกับวิธีใช้ {name} (Misskey) หรือออกจากบทช่วยสอนแล้วเริ่มใช้งานได้ทันที"
|
||||
@@ -1639,7 +1694,7 @@ _initialTutorial:
|
||||
localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
|
||||
_cw:
|
||||
title: "คำเตือนเกี่ยวกับเนื้อหา"
|
||||
description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
|
||||
description: "เนื้อหาที่เขียนใน “ข้อความเกริ่น” จะแสดงแทนเนื้อหาหลัก ต้องกด “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
|
||||
_exampleNote:
|
||||
cw: " ห้ามดู ระวังหิว"
|
||||
note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
|
||||
@@ -1991,7 +2046,7 @@ _role:
|
||||
isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข"
|
||||
isPublic: "ทำให้บทบาทเปิดเผยต่อสาธารณะ"
|
||||
descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
options: "ตัวเลือก"
|
||||
policies: "นโยบาย"
|
||||
baseRole: "แม่แบบบทบาท"
|
||||
useBaseValue: "ใช้ตามแม่แบบบทบาท"
|
||||
@@ -2025,6 +2080,7 @@ _role:
|
||||
canManageAvatarDecorations: "จัดการตกแต่งอวตาร"
|
||||
driveCapacity: "ความจุของไดรฟ์"
|
||||
maxFileSize: "ขนาดไฟล์สูงสุดที่สามารถอัปโหลดได้"
|
||||
maxFileSize_caption: "รีเวิร์สพร็อกซี, CDN และคอมโพเนนต์หน้าบ้านอื่นๆ อาจมีค่าการตั้งค่าของตนเอง"
|
||||
alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
|
||||
canUpdateBioMedia: "อนุญาตให้เปลี่ยนไอคอนประจำตัวและแบนเนอร์"
|
||||
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
|
||||
@@ -2386,7 +2442,7 @@ _permissions:
|
||||
"read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล"
|
||||
"read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางในฐานข้อมูล"
|
||||
"read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้"
|
||||
"read:admin:meta": "ดูข้อมูลอภิพันธุ์ของอินสแตนซ์"
|
||||
"read:admin:meta": "ดูเมทาเดต้าของอินสแตนซ์"
|
||||
"write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้"
|
||||
"write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้"
|
||||
"write:admin:send-email": "ส่งอีเมล"
|
||||
@@ -2397,7 +2453,7 @@ _permissions:
|
||||
"write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
|
||||
"write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
|
||||
"write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้"
|
||||
"write:admin:meta": "จัดการข้อมูลอภิพันธุ์ของอินสแตนซ์"
|
||||
"write:admin:meta": "จัดการเมทาเดต้าของอินสแตนซ์"
|
||||
"write:admin:user-note": "จัดการโน้ตการกลั่นกรอง"
|
||||
"write:admin:roles": "จัดการบทบาท"
|
||||
"read:admin:roles": "ดูบทบาท"
|
||||
@@ -2443,6 +2499,7 @@ _auth:
|
||||
scopeUser: "กำลังดำเนินการในฐานะผู้ใช้ต่อไปนี้"
|
||||
pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน"
|
||||
byClickingYouWillBeRedirectedToThisUrl: "หากอนุญาตการเข้าถึง ระบบจะเปลี่ยนเส้นทางไปยัง URL ด้านล่างโดยอัตโนมัติ"
|
||||
alreadyAuthorized: "แอปพลิเคชันนี้ได้รับอนุญาตให้เข้าถึงแล้ว"
|
||||
_antennaSources:
|
||||
all: "โน้ตทั้งหมด"
|
||||
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
|
||||
@@ -2491,11 +2548,40 @@ _widgets:
|
||||
chat: "แชตเลย"
|
||||
_widgetOptions:
|
||||
showHeader: "แสดงส่วนหัว"
|
||||
transparent: "ทำพื้นหลังโปรงใส"
|
||||
height: "ความสูง"
|
||||
_button:
|
||||
colored: "สี"
|
||||
_clock:
|
||||
size: "ขนาด"
|
||||
thickness: "ความหนาเข็ม"
|
||||
thicknessThin: "บาง"
|
||||
thicknessMedium: "ปานกลาง"
|
||||
thicknessThick: "หนา"
|
||||
graduations: "ขีดบอกค่าบนหน้าปัด"
|
||||
graduationDots: "จุด"
|
||||
graduationArabic: "เลขอารบิก"
|
||||
fadeGraduations: "เฟดหน้าปัด"
|
||||
sAnimation: "การเคลื่อนไหวของเข็มวินาที"
|
||||
sAnimationElastic: "สมจริง"
|
||||
sAnimationEaseOut: "ลื่นๆ"
|
||||
twentyFour: "ระบบ 24 ชั่วโมง"
|
||||
labelTime: "เวลา"
|
||||
labelTz: "เขตเวลา"
|
||||
labelTimeAndTz: "เวลาและเขตเวลา"
|
||||
timezone: "เขตเวลา"
|
||||
showMs: "แสดงมิลลิวินาที"
|
||||
showLabel: "แสดงป้าย"
|
||||
_jobQueue:
|
||||
sound: "เล่นเสียง"
|
||||
_rss:
|
||||
url: "URL ของฟีด RSS"
|
||||
refreshIntervalSec: "ห้วงอัปเดต (วินาที)"
|
||||
maxEntries: "จำนวนที่แสดงได้สูงสุด"
|
||||
_rssTicker:
|
||||
shuffle: "สุ่มลำดับ"
|
||||
duration: "ความเร็วทิกเกอร์ (วินาที)"
|
||||
reverse: "วิ่งไปอีกทาง"
|
||||
_birthdayFollowings:
|
||||
period: "ระยะเวลา"
|
||||
_cw:
|
||||
@@ -2542,9 +2628,20 @@ _postForm:
|
||||
replyPlaceholder: "ตอบกลับโน้ตนี้..."
|
||||
quotePlaceholder: "อ้างโน้ตนี้..."
|
||||
channelPlaceholder: "โพสต์ลงช่อง..."
|
||||
showHowToUse: "แสดงวิธีใช้ฟอร์ม"
|
||||
_howToUse:
|
||||
content_title: "เนื้อความ"
|
||||
content_description: "ป้อนเนื้อหาที่จะโพสต์"
|
||||
toolbar_title: "แถบเครื่องมือ"
|
||||
toolbar_description: "สามารถแนบไฟล์หรือแบบสอบถาม ตั้งข้อความเกริ่นหรือแฮชแท็ก แทรกเอโมจิหรือการกล่าวถึง เป็นต้น"
|
||||
account_title: "เมนูบัญชี"
|
||||
account_description: "สามารถสลับบัญชีที่ใช้โพสต์ หรือดูรายการฉบับร่างและโพสต์กำหนดเวลาไว้ซึ่งบันทึกไว้ในบัญชีได้"
|
||||
visibility_title: "การมองเห็น"
|
||||
visibility_description: "สามารถตั้งค่าขอบเขตการเผยแพร่โน้ตได้"
|
||||
menu_title: "เมนู"
|
||||
menu_description: "สามารถบันทึกเป็นฉบับร่าง ตั้งเวลาการโพสต์ ตั้งค่ารีแอคชั่น และดำเนินการอื่นๆ ได้"
|
||||
submit_title: "ปุ่มโพสต์"
|
||||
submit_description: "กดปุ่มนั้นเพื่อโพสต์โน้ต หรือกด Ctrl + Enter / Cmd + Return เพื่อโพสต์ก็ได้เช่นกัน"
|
||||
_placeholders:
|
||||
a: "ตอนนี้เป็นยังไงบ้าง?"
|
||||
b: "มีอะไรเกิดขึ้นหรือเปล่า?"
|
||||
@@ -2560,7 +2657,7 @@ _profile:
|
||||
metadata: "ข้อมูลเพิ่มเติม"
|
||||
metadataEdit: "แก้ไขข้อมูลเพิ่มเติม"
|
||||
metadataDescription: "ใช้สิ่งเหล่านี้ คุณสามารถแสดงฟิลด์ข้อมูลเพิ่มเติมในโปรไฟล์ของคุณ"
|
||||
metadataLabel: "ป้ายชื่อ"
|
||||
metadataLabel: "ป้าย"
|
||||
metadataContent: "เนื้อหา"
|
||||
changeAvatar: "เปลี่ยนไอคอนประจำตัว"
|
||||
changeBanner: "เปลี่ยนแบนเนอร์"
|
||||
@@ -2721,6 +2818,8 @@ _notification:
|
||||
quote: "อ้างอิง"
|
||||
reaction: "รีแอคชั่น"
|
||||
pollEnded: "โพลสิ้นสุดแล้ว"
|
||||
scheduledNotePosted: "โพสต์กำหนดเวลาสำเร็จ"
|
||||
scheduledNotePostFailed: "โพสต์กำหนดเวลาล้มเหลว"
|
||||
receiveFollowRequest: "ได้รับคำร้องขอติดตาม"
|
||||
followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
|
||||
roleAssigned: "ให้บทบาท"
|
||||
@@ -2760,6 +2859,14 @@ _deck:
|
||||
usedAsMinWidthWhenFlexible: "ความกว้างขั้นต่ำนั้นจะถูกใช้งานสำหรับสิ่งนี้เมื่อเปิดใช้งานตัวเลือก \"ปรับความกว้างอัตโนมัติ\" หากเลือกเปิดใช้งานแล้ว"
|
||||
flexible: "ปรับความกว้างอัตโนมัติ"
|
||||
enableSyncBetweenDevicesForProfiles: "เปิดใช้งานการซิงค์ข้อมูลโปรไฟล์ระหว่างอุปกรณ์"
|
||||
showHowToUse: "แสดงวิธีใช้ UI"
|
||||
_howToUse:
|
||||
addColumn_title: "เพิ่มคอลัมน์"
|
||||
addColumn_description: "สามารถเลือกประเภทของคอลัมน์แล้วเพิ่มได้"
|
||||
settings_title: "ตั้งค่า UI"
|
||||
settings_description: "สามารถตั้งค่ารายละเอียดของ UI แบบเด็คได้"
|
||||
switchProfile_title: "สลับโปรไฟล์"
|
||||
switchProfile_description: "สามารถบันทึกเลย์เอาต์ของ UI เป็นโปรไฟล์ และสลับใช้งานได้ทุกเมื่อ"
|
||||
_columns:
|
||||
main: "หลัก"
|
||||
widgets: "วิดเจ็ต"
|
||||
@@ -2820,6 +2927,8 @@ _abuseReport:
|
||||
notifiedWebhook: "Webhook ที่ใช้"
|
||||
deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?"
|
||||
_moderationLogTypes:
|
||||
clearQueue: "ล้างคิว"
|
||||
promoteQueue: "ดันงานในคิวอีกครั้ง"
|
||||
createRole: "สร้างบทบาทแล้ว"
|
||||
deleteRole: "ลบบทบาทแล้ว"
|
||||
updateRole: "อัปเดตบทบาทแล้ว"
|
||||
@@ -3063,7 +3172,7 @@ _customEmojisManager:
|
||||
uploadSettingDescription: "สามารถกำหนดพฤติกรรมขณะอัปโหลดเอโมจิจากหน้าจอนี้ได้"
|
||||
directoryToCategoryLabel: "ป้อนชื่อไดเรกทอรีเป็น \"category\""
|
||||
directoryToCategoryCaption: "เมื่อทำการลากและวางไดเรกทอรี ชื่อจะถูกป้อนเป็น \"category\""
|
||||
confirmRegisterEmojisDescription: "จะลงทะเบียนเอโมจิที่แสดงในรายการเป็นเอโมจิแบบกำหนดเองใหม่\nดำเนินการต่อหรือไม่? (เพื่อหลีกเลี่ยงภาระโหลดหนัก ระบบจะสามารถลงทะเบียนอีโมจิได้สูงสุด {count} รายการต่อครั้ง)"
|
||||
confirmRegisterEmojisDescription: "จะลงทะเบียนเอโมจิที่แสดงในรายการเป็นเอโมจิแบบกำหนดเองใหม่\nดำเนินการต่อหรือไม่? (เพื่อหลีกเลี่ยงภาระโหลดหนัก ระบบจะสามารถลงทะเบียนเอโมจิได้สูงสุด {count} รายการต่อครั้ง)"
|
||||
confirmClearEmojisDescription: "ต้องการยกเลิกการแก้ไขและล้างรายการเอโมจิที่แสดงอยู่หรือไม่?"
|
||||
confirmUploadEmojisDescription: "จะอัปโหลดไฟล์ {count} รายการที่ลากและวางไปยังไดรฟ์ ดำเนินการหรือไม่?"
|
||||
_embedCodeGen:
|
||||
@@ -3214,6 +3323,7 @@ _watermarkEditor:
|
||||
title: "แก้ไขลายน้ำ"
|
||||
cover: "ซ้อนทับทั่วทั้งพื้นที่"
|
||||
repeat: "ปูให้เต็มพื้นที่"
|
||||
preserveBoundingRect: "ปรับไม่ให้ล้นขอบเมื่อหมุน"
|
||||
opacity: "ความทึบแสง"
|
||||
scale: "ขนาด"
|
||||
text: "ข้อความ"
|
||||
@@ -3235,10 +3345,12 @@ _watermarkEditor:
|
||||
polkadotSubDotRadius: "ขนาดของจุดรอง"
|
||||
polkadotSubDotDivisions: "จำนวนจุดรอง"
|
||||
leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_imageEffector:
|
||||
title: "เอฟเฟกต์"
|
||||
addEffect: "เพิ่มเอฟเฟกต์"
|
||||
discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_fxs:
|
||||
chromaticAberration: "ความคลาดสี"
|
||||
glitch: "กลิตช์"
|
||||
|
||||
@@ -164,7 +164,7 @@ selectAntenna: "选择天线"
|
||||
editAntenna: "编辑天线"
|
||||
createAntenna: "创建天线"
|
||||
selectWidget: "选择小工具"
|
||||
editWidgets: "编辑部件"
|
||||
editWidgets: "编辑小工具"
|
||||
editWidgetsExit: "完成编辑"
|
||||
customEmojis: "自定义表情符号"
|
||||
emoji: "表情符号"
|
||||
@@ -543,6 +543,7 @@ regenerate: "重新生成"
|
||||
fontSize: "字体大小"
|
||||
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
|
||||
limitTo: "上限为 {x}"
|
||||
showMediaListByGridInWideArea: "在大屏幕上并排显示媒体列表"
|
||||
noFollowRequests: "没有关注请求"
|
||||
openImageInNewTab: "在新标签页中打开图片"
|
||||
dashboard: "管理面板"
|
||||
@@ -831,7 +832,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。"
|
||||
newVersionOfClientAvailable: "新版本的客户端可用。"
|
||||
usageAmount: "使用量"
|
||||
capacity: "容量"
|
||||
inUse: "使用中"
|
||||
inUse: "已使用"
|
||||
editCode: "编辑代码"
|
||||
apply: "应用"
|
||||
receiveAnnouncementFromInstance: "从服务器接收通知"
|
||||
@@ -2255,7 +2256,7 @@ _menuDisplay:
|
||||
top: "顶部"
|
||||
hide: "隐藏"
|
||||
_wordMute:
|
||||
muteWords: "要屏蔽的词"
|
||||
muteWords: "要折叠的词"
|
||||
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
muteWordsDescription2: "正则表达式用斜线包裹"
|
||||
_instanceMute:
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "再次生成"
|
||||
fontSize: "字體大小"
|
||||
mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度"
|
||||
limitTo: "上限為 {x}"
|
||||
showMediaListByGridInWideArea: "當畫面寬度較寬時,將媒體清單以橫向排列顯示"
|
||||
noFollowRequests: "沒有追隨您的請求"
|
||||
openImageInNewTab: "於新分頁中開啟圖片"
|
||||
dashboard: "儀表板"
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"packages/i18n",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-mahjong",
|
||||
"packages/icons-subsetter",
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend-builder",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Mahjong1706234054207 {
|
||||
name = 'Mahjong1706234054207'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "mahjong_game" ("id" character varying(32) NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "endedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32), "user2Id" character varying(32), "user3Id" character varying(32), "user4Id" character varying(32), "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90', "logs" jsonb NOT NULL DEFAULT '[]', CONSTRAINT "PK_77db54c0a9785d387e3fbbdd2f0" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_b98c78761a845b69e6540401264" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_f17b0ba519ae28f188a7915ad6f" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_64314ffd3cb59475b0d06330058" FOREIGN KEY ("user3Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_58a75f1ea2a810ae3986f72a0e3" FOREIGN KEY ("user4Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_58a75f1ea2a810ae3986f72a0e3"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_64314ffd3cb59475b0d06330058"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_f17b0ba519ae28f188a7915ad6f"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_b98c78761a845b69e6540401264"`);
|
||||
await queryRunner.query(`DROP TABLE "mahjong_game"`);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@
|
||||
"@swc/cli": "0.7.9",
|
||||
"@swc/core": "1.15.7",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@types/redis-info": "3.0.3",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
@@ -134,7 +135,6 @@
|
||||
"mime-types": "3.0.2",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-mahjong": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.6",
|
||||
"nested-property": "4.0.0",
|
||||
@@ -154,6 +154,7 @@
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.23.0",
|
||||
"redis-info": "3.1.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
|
||||
@@ -14,56 +14,24 @@ import { fork } from 'node:child_process';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import * as http from 'node:http';
|
||||
import * as fs from 'node:fs/promises';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const SAMPLE_COUNT = 3; // Number of samples to measure
|
||||
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||
|
||||
const keys = {
|
||||
VmPeak: 0,
|
||||
VmSize: 0,
|
||||
VmHWM: 0,
|
||||
VmRSS: 0,
|
||||
VmData: 0,
|
||||
VmStk: 0,
|
||||
VmExe: 0,
|
||||
VmLib: 0,
|
||||
VmPTE: 0,
|
||||
VmSwap: 0,
|
||||
};
|
||||
|
||||
async function getMemoryUsage(pid) {
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
|
||||
const result = {};
|
||||
for (const key of Object.keys(keys)) {
|
||||
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
||||
if (match) {
|
||||
result[key] = parseInt(match[1], 10);
|
||||
} else {
|
||||
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function measureMemory() {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Start the Misskey backend server using fork to enable IPC
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
||||
cwd: join(__dirname, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
MK_DISABLE_CLUSTERING: '1',
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
execArgv: [...process.execArgv, '--expose-gc'],
|
||||
});
|
||||
|
||||
let serverReady = false;
|
||||
@@ -89,40 +57,6 @@ async function measureMemory() {
|
||||
process.stderr.write(`[server error] ${err}\n`);
|
||||
});
|
||||
|
||||
async function triggerGc() {
|
||||
const ok = new Promise((resolve) => {
|
||||
serverProcess.once('message', (message) => {
|
||||
if (message === 'gc ok') resolve();
|
||||
});
|
||||
});
|
||||
|
||||
serverProcess.send('gc');
|
||||
|
||||
await ok;
|
||||
|
||||
await setTimeout(1000);
|
||||
}
|
||||
|
||||
function createRequest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
host: 'localhost',
|
||||
port: 61812,
|
||||
path: '/api/meta',
|
||||
method: 'POST',
|
||||
}, (res) => {
|
||||
res.on('data', () => { });
|
||||
res.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for server to be ready or timeout
|
||||
const startupStartTime = Date.now();
|
||||
while (!serverReady) {
|
||||
@@ -139,23 +73,46 @@ async function measureMemory() {
|
||||
// Wait for memory to settle
|
||||
await setTimeout(MEMORY_SETTLE_TIME);
|
||||
|
||||
// Get memory usage from the server process via /proc
|
||||
const pid = serverProcess.pid;
|
||||
let memoryInfo;
|
||||
|
||||
const beforeGc = await getMemoryUsage(pid);
|
||||
try {
|
||||
const fs = await import('node:fs/promises');
|
||||
|
||||
await triggerGc();
|
||||
// Read /proc/[pid]/status for detailed memory info
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
||||
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
|
||||
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
|
||||
|
||||
const afterGc = await getMemoryUsage(pid);
|
||||
memoryInfo = {
|
||||
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
|
||||
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
|
||||
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
// Fallback: use ps command
|
||||
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
|
||||
|
||||
// create some http requests to simulate load
|
||||
const REQUEST_COUNT = 10;
|
||||
await Promise.all(
|
||||
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
|
||||
);
|
||||
|
||||
await triggerGc();
|
||||
|
||||
const afterRequest = await getMemoryUsage(pid);
|
||||
const { execSync } = await import('node:child_process');
|
||||
try {
|
||||
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
|
||||
const rssKb = parseInt(ps.trim(), 10);
|
||||
memoryInfo = {
|
||||
rss: rssKb * 1024,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
};
|
||||
} catch {
|
||||
memoryInfo = {
|
||||
rss: null,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
error: 'Could not measure memory',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
serverProcess.kill('SIGTERM');
|
||||
@@ -178,51 +135,15 @@ async function measureMemory() {
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
beforeGc,
|
||||
afterGc,
|
||||
afterRequest,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 直列の方が時間的に分散されて正確そうだから直列でやる
|
||||
const results = [];
|
||||
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
||||
const res = await measureMemory();
|
||||
results.push(res);
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
const beforeGc = structuredClone(keys);
|
||||
const afterGc = structuredClone(keys);
|
||||
const afterRequest = structuredClone(keys);
|
||||
for (const res of results) {
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] += res.beforeGc[key];
|
||||
afterGc[key] += res.afterGc[key];
|
||||
afterRequest[key] += res.afterRequest[key];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
|
||||
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
|
||||
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
|
||||
}
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
beforeGc,
|
||||
afterGc,
|
||||
afterRequest,
|
||||
startupTimeMs: startupTime,
|
||||
memory: memoryInfo,
|
||||
};
|
||||
|
||||
// Output as JSON to stdout
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
measureMemory().catch((err) => {
|
||||
console.error(JSON.stringify({
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -86,18 +86,6 @@ if (!envOption.disableClustering) {
|
||||
ev.mount();
|
||||
}
|
||||
|
||||
process.on('message', msg => {
|
||||
if (msg === 'gc') {
|
||||
if (global.gc != null) {
|
||||
logger.info('Manual GC triggered');
|
||||
global.gc();
|
||||
if (process.send != null) process.send('gc ok');
|
||||
} else {
|
||||
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
readyRef.value = true;
|
||||
|
||||
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
||||
|
||||
@@ -78,7 +78,6 @@ import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { ChatService } from './ChatService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
import { MahjongService } from './MahjongService.js';
|
||||
import { PageService } from './PageService.js';
|
||||
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
@@ -229,7 +228,6 @@ const $ChannelMutingService: Provider = { provide: 'ChannelMutingService', useEx
|
||||
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $MahjongService: Provider = { provide: 'MahjongService', useExisting: MahjongService };
|
||||
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
@@ -383,7 +381,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
MahjongService,
|
||||
PageService,
|
||||
|
||||
ChartLoggerService,
|
||||
@@ -534,7 +531,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$MahjongService,
|
||||
$PageService,
|
||||
|
||||
$ChartLoggerService,
|
||||
@@ -685,7 +681,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
MahjongService,
|
||||
PageService,
|
||||
|
||||
FederationChart,
|
||||
@@ -834,7 +829,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$MahjongService,
|
||||
$PageService,
|
||||
|
||||
$FederationChart,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
@@ -202,78 +201,6 @@ export interface ReversiGameEventTypes {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MahjongRoomEventTypes {
|
||||
joined: {
|
||||
index: number;
|
||||
user: Packed<'UserLite'> | null;
|
||||
};
|
||||
changeReadyStates: {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
user3: boolean;
|
||||
user4: boolean;
|
||||
};
|
||||
started: {
|
||||
room: Packed<'MahjongRoomDetailed'>;
|
||||
};
|
||||
nextKyoku: {
|
||||
room: Packed<'MahjongRoomDetailed'>;
|
||||
};
|
||||
tsumo: {
|
||||
house: Mmj.House;
|
||||
tile: number;
|
||||
};
|
||||
dahai: {
|
||||
house: Mmj.House;
|
||||
tile: number;
|
||||
riichi: boolean;
|
||||
};
|
||||
dahaiAndTsumo: {
|
||||
dahaiHouse: Mmj.House;
|
||||
dahaiTile: number;
|
||||
tsumoTile: number;
|
||||
riichi: boolean;
|
||||
};
|
||||
ponned: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number];
|
||||
};
|
||||
kanned: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
};
|
||||
ciied: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number];
|
||||
};
|
||||
ronned: {
|
||||
callers: Mmj.House[];
|
||||
callee: Mmj.House;
|
||||
handTiles: Record<Mmj.House, number[]>;
|
||||
};
|
||||
ryuukyoku: object;
|
||||
ankanned: {
|
||||
house: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
};
|
||||
kakanned: {
|
||||
house: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
from: Mmj.House;
|
||||
};
|
||||
tsumoHora: {
|
||||
house: Mmj.House;
|
||||
handTiles: number[];
|
||||
tsumoTile: number;
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||
@@ -393,10 +320,6 @@ export type GlobalEvents = {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
|
||||
};
|
||||
mahjongRoom: {
|
||||
name: `mahjongRoomStream:${string}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MahjongRoomEventTypes>>;
|
||||
};
|
||||
};
|
||||
|
||||
// API event definitions
|
||||
@@ -506,9 +429,4 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishMahjongRoomStream<K extends keyof MahjongRoomEventTypes>(roomId: string, type: K, value?: MahjongRoomEventTypes[K]): void {
|
||||
this.publish(`mahjongRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,734 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { IsNull, LessThan, MoreThan } from 'typeorm';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import type {
|
||||
MiMahjongGame,
|
||||
MahjongGamesRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 10; // 10sec
|
||||
const TURN_TIMEOUT_MS = 1000 * 30; // 30sec
|
||||
const NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
|
||||
type Room = {
|
||||
id: string;
|
||||
user1Id: MiUser['id'];
|
||||
user2Id: MiUser['id'] | null;
|
||||
user3Id: MiUser['id'] | null;
|
||||
user4Id: MiUser['id'] | null;
|
||||
user1: Packed<'UserLite'> | null;
|
||||
user2: Packed<'UserLite'> | null;
|
||||
user3: Packed<'UserLite'> | null;
|
||||
user4: Packed<'UserLite'> | null;
|
||||
user1Ai?: boolean;
|
||||
user2Ai?: boolean;
|
||||
user3Ai?: boolean;
|
||||
user4Ai?: boolean;
|
||||
user1Ready: boolean;
|
||||
user2Ready: boolean;
|
||||
user3Ready: boolean;
|
||||
user4Ready: boolean;
|
||||
user1Offline?: boolean;
|
||||
user2Offline?: boolean;
|
||||
user3Offline?: boolean;
|
||||
user4Offline?: boolean;
|
||||
isStarted?: boolean;
|
||||
timeLimitForEachTurn: number;
|
||||
|
||||
gameState?: Mmj.MasterState;
|
||||
};
|
||||
|
||||
type CallingAnswers = {
|
||||
pon: null | boolean;
|
||||
cii: null | false | 'x__' | '_x_' | '__x';
|
||||
kan: null | boolean;
|
||||
ron: {
|
||||
e: null | boolean;
|
||||
s: null | boolean;
|
||||
w: null | boolean;
|
||||
n: null | boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type NextKyokuConfirmation = {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
user3: boolean;
|
||||
user4: boolean;
|
||||
};
|
||||
|
||||
function getUserIdOfHouse(room: Room, mj: Mmj.MasterGameEngine, house: Mmj.House): MiUser['id'] {
|
||||
return mj.user1House === house ? room.user1Id : mj.user2House === house ? room.user2Id : mj.user3House === house ? room.user3Id : room.user4Id;
|
||||
}
|
||||
|
||||
function getHouseOfUserId(room: Room, mj: Mmj.MasterGameEngine, userId: MiUser['id']): Mmj.House {
|
||||
return userId === room.user1Id ? mj.user1House : userId === room.user2Id ? mj.user2House : userId === room.user3Id ? mj.user3House : mj.user4House;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||
private notificationService: NotificationService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
//@Inject(DI.mahjongGamesRepository)
|
||||
//private mahjongGamesRepository: MahjongGamesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
async onModuleInit() {
|
||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async saveRoom(room: Room) {
|
||||
await this.redisClient.set(`mahjong:room:${room.id}`, JSON.stringify(room), 'EX', 60 * 30);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createRoom(user: MiUser): Promise<Room> {
|
||||
const room: Room = {
|
||||
id: this.idService.gen(),
|
||||
user1Id: user.id,
|
||||
user2Id: null,
|
||||
user3Id: null,
|
||||
user4Id: null,
|
||||
user1: await this.userEntityService.pack(user),
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
user3Ready: false,
|
||||
user4Ready: false,
|
||||
timeLimitForEachTurn: 30,
|
||||
};
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoom(id: Room['id']): Promise<Room | null> {
|
||||
const room = await this.redisClient.get(`mahjong:room:${id}`);
|
||||
if (!room) return null;
|
||||
const parsed = JSON.parse(room);
|
||||
return {
|
||||
...parsed,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async joinRoom(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id === user.id) return room;
|
||||
if (room.user2Id === user.id) return room;
|
||||
if (room.user3Id === user.id) return room;
|
||||
if (room.user4Id === user.id) return room;
|
||||
if (room.user2Id === null) {
|
||||
room.user2Id = user.id;
|
||||
room.user2 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: room.user2 });
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id === null) {
|
||||
room.user3Id = user.id;
|
||||
room.user3 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: room.user3 });
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id === null) {
|
||||
room.user4Id = user.id;
|
||||
room.user4 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 4, user: room.user4 });
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addAi(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id !== user.id) throw new Error('access denied');
|
||||
|
||||
if (room.user2Id == null && !room.user2Ai) {
|
||||
room.user2Ai = true;
|
||||
room.user2Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: null });
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id == null && !room.user3Ai) {
|
||||
room.user3Ai = true;
|
||||
room.user3Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: null });
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id == null && !room.user4Ai) {
|
||||
room.user4Ai = true;
|
||||
room.user4Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 4, user: null });
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async leaveRoom(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id === user.id) {
|
||||
room.user1Id = null;
|
||||
room.user1 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user2Id === user.id) {
|
||||
room.user2Id = null;
|
||||
room.user2 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id === user.id) {
|
||||
room.user3Id = null;
|
||||
room.user3 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id === user.id) {
|
||||
room.user4Id = null;
|
||||
room.user4 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async changeReadyState(roomId: Room['id'], user: MiUser, ready: boolean): Promise<void> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
if (room.user1Id === user.id) {
|
||||
room.user1Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user2Id === user.id) {
|
||||
room.user2Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user3Id === user.id) {
|
||||
room.user3Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user4Id === user.id) {
|
||||
room.user4Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'changeReadyStates', {
|
||||
user1: room.user1Ready,
|
||||
user2: room.user2Ready,
|
||||
user3: room.user3Ready,
|
||||
user4: room.user4Ready,
|
||||
});
|
||||
|
||||
if (room.user1Ready && room.user2Ready && room.user3Ready && room.user4Ready) {
|
||||
await this.startGame(room);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async startGame(room: Room) {
|
||||
if (!room.user1Ready || !room.user2Ready || !room.user3Ready || !room.user4Ready) {
|
||||
throw new Error('Not ready');
|
||||
}
|
||||
|
||||
room.gameState = Mmj.MasterGameEngine.createInitialState();
|
||||
room.isStarted = true;
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'started', { room: room });
|
||||
|
||||
this.kyokuStarted(room);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private kyokuStarted(room: Room) {
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
|
||||
this.waitForTurn(room, mj.turn, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async answer(room: Room, mj: Mmj.MasterGameEngine, answers: CallingAnswers) {
|
||||
const res = mj.commit_resolveCallingInterruption({
|
||||
pon: answers.pon ?? false,
|
||||
cii: answers.cii ?? false,
|
||||
kan: answers.kan ?? false,
|
||||
ron: [...(answers.ron.e ? ['e'] : []), ...(answers.ron.s ? ['s'] : []), ...(answers.ron.w ? ['w'] : []), ...(answers.ron.n ? ['n'] : [])] as Mmj.House[],
|
||||
});
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
switch (res.type) {
|
||||
case 'tsumo':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ponned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { caller: res.caller, callee: res.callee, tiles: res.tiles });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'kanned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'kanned', { caller: res.caller, callee: res.callee, tiles: res.tiles, rinsyan: res.rinsyan });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ciied':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ciied', { caller: res.caller, callee: res.callee, tiles: res.tiles });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ronned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ronned', {
|
||||
callers: res.callers,
|
||||
callee: res.callee,
|
||||
handTiles: {
|
||||
e: mj.handTiles.e,
|
||||
s: mj.handTiles.s,
|
||||
w: mj.handTiles.w,
|
||||
n: mj.handTiles.n,
|
||||
},
|
||||
});
|
||||
this.endKyoku(room, mj);
|
||||
break;
|
||||
case 'ryuukyoku':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
|
||||
});
|
||||
this.endKyoku(room, mj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async endKyoku(room: Room, mj: Mmj.MasterGameEngine) {
|
||||
const confirmation: NextKyokuConfirmation = {
|
||||
user1: false,
|
||||
user2: false,
|
||||
user3: false,
|
||||
user4: false,
|
||||
};
|
||||
this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
if (confirmationRaw == null) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
|
||||
const allConfirmed = confirmation.user1 && confirmation.user2 && confirmation.user3 && confirmation.user4;
|
||||
if (allConfirmed || (Date.now() - waitingStartedAt > NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS)) {
|
||||
await this.redisClient.del(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
clearInterval(interval);
|
||||
this.nextKyoku(room, mj);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async nextKyoku(room: Room, mj: Mmj.MasterGameEngine) {
|
||||
const res = mj.commit_nextKyoku();
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'nextKyoku', {
|
||||
room: room,
|
||||
});
|
||||
this.kyokuStarted(room);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async dahai(room: Room, mj: Mmj.MasterGameEngine, house: Mmj.House, tile: Mmj.TileId, riichi = false) {
|
||||
const res = mj.commit_dahai(house, tile, riichi);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id));
|
||||
|
||||
if (res.ryuukyoku) {
|
||||
this.endKyoku(room, mj);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
|
||||
});
|
||||
} else if (res.asking) {
|
||||
const answers: CallingAnswers = {
|
||||
pon: null,
|
||||
cii: null,
|
||||
kan: null,
|
||||
ron: {
|
||||
e: null,
|
||||
s: null,
|
||||
w: null,
|
||||
n: null,
|
||||
},
|
||||
};
|
||||
|
||||
// リーチ中はポン、チー、カンできない
|
||||
if (res.canPonHouse != null && mj.riichis[res.canPonHouse]) {
|
||||
answers.pon = false;
|
||||
}
|
||||
if (res.canCiiHouse != null && mj.riichis[res.canCiiHouse]) {
|
||||
answers.cii = false;
|
||||
}
|
||||
if (res.canKanHouse != null && mj.riichis[res.canKanHouse]) {
|
||||
answers.kan = false;
|
||||
}
|
||||
|
||||
if (aiHouses.includes(res.canPonHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
answers.pon = Math.random() < 0.25;
|
||||
}
|
||||
if (aiHouses.includes(res.canCiiHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
//answers.cii = Math.random() < 0.25;
|
||||
answers.cii = false;
|
||||
}
|
||||
if (aiHouses.includes(res.canKanHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
answers.kan = Math.random() < 0.25;
|
||||
}
|
||||
for (const h of res.canRonHouses) {
|
||||
if (aiHouses.includes(h)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
}
|
||||
}
|
||||
|
||||
this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(answers));
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('arienai (gameCallingAsking)');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
const allAnswered = !(
|
||||
(res.canPonHouse != null && currentAnswers.pon == null) ||
|
||||
(res.canCiiHouse != null && currentAnswers.cii == null) ||
|
||||
(res.canKanHouse != null && currentAnswers.kan == null) ||
|
||||
(res.canRonHouses.includes('e') && currentAnswers.ron.e == null) ||
|
||||
(res.canRonHouses.includes('s') && currentAnswers.ron.s == null) ||
|
||||
(res.canRonHouses.includes('w') && currentAnswers.ron.w == null) ||
|
||||
(res.canRonHouses.includes('n') && currentAnswers.ron.n == null)
|
||||
);
|
||||
if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) {
|
||||
console.log(allAnswered ? 'ask all answerd' : 'ask timeout');
|
||||
await this.redisClient.del(`mahjong:gameCallingAsking:${room.id}`);
|
||||
clearInterval(interval);
|
||||
this.answer(room, mj, currentAnswers);
|
||||
return;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: house, tile, riichi });
|
||||
} else {
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { dahaiHouse: house, dahaiTile: tile, tsumoTile: res.tsumoTile, riichi });
|
||||
|
||||
this.waitForTurn(room, res.next, mj);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async confirmNextKyoku(roomId: Room['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
if (confirmationRaw == null) return;
|
||||
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
|
||||
if (user.id === room.user1Id) confirmation.user1 = true;
|
||||
if (user.id === room.user2Id) confirmation.user2 = true;
|
||||
if (user.id === room.user3Id) confirmation.user3 = true;
|
||||
if (user.id === room.user4Id) confirmation.user4 = true;
|
||||
await this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId, riichi = false) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
await this.dahai(room, mj, myHouse, tile, riichi);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_ankan(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_ankan(myHouse, tile);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ankanned', { house: myHouse, tiles: res.tiles, rinsyan: res.rinsyan });
|
||||
|
||||
this.waitForTurn(room, myHouse, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_kakan(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_kakan(myHouse, tile);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'kakanned', { house: myHouse, tiles: res.tiles, rinsyan: res.rinsyan, from: res.from });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_tsumoHora(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_tsumoHora(myHouse);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumoHora', { house: myHouse, handTiles: res.handTiles, tsumoTile: res.tsumoTile });
|
||||
|
||||
this.endKyoku(room, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_ronHora(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.ron[myHouse] = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_pon(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.pon = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_kan(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.kan = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_cii(roomId: MiMahjongGame['id'], user: MiUser, pattern: 'x__' | '_x_' | '__x') {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.cii = pattern;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_nop(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
if (mj.askings.pon?.caller === myHouse) currentAnswers.pon = false;
|
||||
if (mj.askings.cii?.caller === myHouse) currentAnswers.cii = false;
|
||||
if (mj.askings.kan?.caller === myHouse) currentAnswers.kan = false;
|
||||
if (mj.askings.ron != null && mj.askings.ron.callers.includes(myHouse)) currentAnswers.ron[myHouse] = false;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
/**
|
||||
* プレイヤーの行動(打牌、加槓、暗槓、ツモ和了)を待つ
|
||||
* 制限時間が過ぎたらツモ切り
|
||||
* NOTE: 時間切れチェックが行われたときにタイミングによっては次のwaitingが始まっている場合があることを考慮し、Setに一意のIDを格納する構造としている
|
||||
* @param room
|
||||
* @param house
|
||||
* @param mj
|
||||
*/
|
||||
@bindThis
|
||||
private async waitForTurn(room: Room, house: Mmj.House, mj: Mmj.MasterGameEngine) {
|
||||
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id));
|
||||
|
||||
if (mj.riichis[house]) {
|
||||
// リーチ時はアガリ牌でない限りツモ切り
|
||||
if (!Mmj.isAgarikei(mj.handTileTypes[house])) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (aiHouses.includes(house)) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Math.random().toString(36).slice(2);
|
||||
console.log('waitForTurn', house, id);
|
||||
this.redisClient.sadd(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const waiting = await this.redisClient.sismember(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
if (waiting === 0) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - waitingStartedAt > TURN_TIMEOUT_MS) {
|
||||
await this.redisClient.srem(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
console.log('turn timeout', house, id);
|
||||
clearInterval(interval);
|
||||
const handTiles = mj.handTiles[house];
|
||||
await this.dahai(room, mj, house, handTiles.at(-1));
|
||||
return;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* プレイヤーが行動(打牌、加槓、暗槓、ツモ和了)したら呼ぶ
|
||||
* @param roomId
|
||||
*/
|
||||
@bindThis
|
||||
private async clearTurnWaitingTimer(roomId: Room['id']) {
|
||||
await this.redisClient.del(`mahjong:gameTurnWaiting:${roomId}`);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packState(room: Room, me: MiUser) {
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myIndex = room.user1Id === me.id ? 1 : room.user2Id === me.id ? 2 : room.user3Id === me.id ? 3 : 4;
|
||||
return mj.createPlayerState(myIndex);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoom(room: Room, me: MiUser) {
|
||||
if (room.gameState) {
|
||||
return {
|
||||
...room,
|
||||
gameState: this.packState(room, me),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...room,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MetricsTime, type JobType } from 'bullmq';
|
||||
import { parse as parseRedisInfo } from 'redis-info';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
@@ -85,19 +86,6 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
|
||||
pattern: '0 4 * * *',
|
||||
}];
|
||||
|
||||
function parseRedisInfo(infoText: string): Record<string, string> {
|
||||
const fields = infoText
|
||||
.split('\n')
|
||||
.filter(line => line.length > 0 && !line.startsWith('#'))
|
||||
.map(line => line.trim().split(':'));
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of fields) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
constructor(
|
||||
@@ -902,7 +890,7 @@ export class QueueService {
|
||||
},
|
||||
db: {
|
||||
version: db.redis_version,
|
||||
mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
|
||||
mode: db.redis_mode,
|
||||
runId: db.run_id,
|
||||
processId: db.process_id,
|
||||
port: parseInt(db.tcp_port),
|
||||
|
||||
@@ -14,10 +14,6 @@ import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
|
||||
return ['random', '1', '2'].includes(bw);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameEntityService {
|
||||
constructor(
|
||||
@@ -62,7 +58,7 @@ export class ReversiGameEntityService {
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
@@ -120,7 +116,7 @@ export class ReversiGameEntityService {
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
|
||||
@@ -90,7 +90,6 @@ export const DI = {
|
||||
chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
mahjongGamesRepository: Symbol('mahjongGamesRepository'),
|
||||
noteDraftsRepository: Symbol('noteDraftsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -64,7 +64,6 @@ import {
|
||||
packedMetaDetailedOnlySchema,
|
||||
packedMetaDetailedSchema,
|
||||
packedMetaLiteSchema,
|
||||
packedMetaClientOptionsSchema,
|
||||
} from '@/models/json-schema/meta.js';
|
||||
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
@@ -73,7 +72,6 @@ import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessage
|
||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedMahjongRoomDetailedSchema } from '@/models/json-schema/mahjong-room.js';
|
||||
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
|
||||
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||
|
||||
@@ -137,7 +135,6 @@ export const refs = {
|
||||
MetaLite: packedMetaLiteSchema,
|
||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
MetaClientOptions: packedMetaClientOptionsSchema,
|
||||
UserWebhook: packedUserWebhookSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
@@ -148,7 +145,6 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
MahjongRoomDetailed: packedMahjongRoomDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('mahjong_game')
|
||||
export class MiMahjongGame {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public startedAt: Date | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public endedAt: Date | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user1Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user1: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user2Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user2: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user3Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user3: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user4Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user4: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isEnded: boolean;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public winnerId: MiUser['id'] | null;
|
||||
|
||||
// in sec
|
||||
@Column('smallint', {
|
||||
default: 90,
|
||||
})
|
||||
public timeLimitForEachTurn: number;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: number[][];
|
||||
}
|
||||
@@ -725,11 +725,7 @@ export class MiMeta {
|
||||
@Column('jsonb', {
|
||||
default: { },
|
||||
})
|
||||
public clientOptions: {
|
||||
entrancePageStyle: 'classic' | 'simple';
|
||||
showTimelineForVisitor: boolean;
|
||||
showActivitiesForVisitor: boolean;
|
||||
};
|
||||
public clientOptions: Record<string, any>;
|
||||
}
|
||||
|
||||
export type SoftwareSuspension = {
|
||||
|
||||
@@ -84,7 +84,6 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiMahjongGame,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -545,12 +544,6 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $mahjongGamesRepository: Provider = {
|
||||
provide: DI.mahjongGamesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiMahjongGame),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -630,7 +623,6 @@ const $mahjongGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$mahjongGamesRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -709,7 +701,6 @@ const $mahjongGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$mahjongGamesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
@@ -43,7 +43,6 @@ import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
import { MiGalleryPost } from '@/models/GalleryPost.js';
|
||||
import { MiHashtag } from '@/models/Hashtag.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { MiMahjongGame } from '@/models/MahjongGame.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
import { MiMuting } from '@/models/Muting.js';
|
||||
@@ -174,7 +173,6 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiMahjongGame,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -255,4 +253,3 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type MahjongGamesRepository = Repository<MiMahjongGame> & MiRepository<MiMahjongGame>;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedMahjongRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
startedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
endedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
isStarted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isEnded: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user2Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user3Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user4Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user1: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user2: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user3: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user4: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user1Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user3Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user4Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user3Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user4Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
timeLimitForEachTurn: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -72,7 +72,8 @@ export const packedMetaLiteSchema = {
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
clientOptions: {
|
||||
ref: 'MetaClientOptions',
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
disableRegistration: {
|
||||
type: 'boolean',
|
||||
@@ -396,23 +397,3 @@ export const packedMetaDetailedSchema = {
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const packedMetaClientOptionsSchema = {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
entrancePageStyle: {
|
||||
type: 'string',
|
||||
enum: ['classic', 'simple'],
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
showTimelineForVisitor: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
showActivitiesForVisitor: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -81,7 +81,6 @@ export const packedReversiGameLiteSchema = {
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['random', '1', '2'],
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
@@ -200,7 +199,6 @@ export const packedReversiGameDetailedSchema = {
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['random', '1', '2'],
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -85,7 +85,6 @@ import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiMahjongGame } from '@/models/MahjongGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
|
||||
@@ -255,7 +254,6 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiMahjongGame,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
@@ -31,6 +30,7 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||
@@ -49,7 +49,6 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||
import { MahjongRoomChannel } from './api/stream/channels/mahjong-room.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@Module({
|
||||
@@ -93,7 +92,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
ChatRoomChannel,
|
||||
ReversiChannel,
|
||||
ReversiGameChannel,
|
||||
MahjongRoomChannel,
|
||||
HomeTimelineChannel,
|
||||
HybridTimelineChannel,
|
||||
LocalTimelineChannel,
|
||||
|
||||
@@ -301,12 +301,6 @@ export * as 'invite/create' from './endpoints/invite/create.js';
|
||||
export * as 'invite/delete' from './endpoints/invite/delete.js';
|
||||
export * as 'invite/limit' from './endpoints/invite/limit.js';
|
||||
export * as 'invite/list' from './endpoints/invite/list.js';
|
||||
export * as 'mahjong/cancel-match' from './endpoints/mahjong/cancel-match.js';
|
||||
export * as 'mahjong/create-room' from './endpoints/mahjong/create-room.js';
|
||||
export * as 'mahjong/games' from './endpoints/mahjong/games.js';
|
||||
export * as 'mahjong/join-room' from './endpoints/mahjong/join-room.js';
|
||||
export * as 'mahjong/show-room' from './endpoints/mahjong/show-room.js';
|
||||
export * as 'mahjong/verify' from './endpoints/mahjong/verify.js';
|
||||
export * as 'meta' from './endpoints/meta.js';
|
||||
export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js';
|
||||
export * as 'mute/create' from './endpoints/mute/create.js';
|
||||
|
||||
@@ -428,7 +428,8 @@ export const meta = {
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
clientOptions: {
|
||||
ref: 'MetaClientOptions',
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -68,14 +67,7 @@ export const paramDef = {
|
||||
description: { type: 'string', nullable: true },
|
||||
defaultLightTheme: { type: 'string', nullable: true },
|
||||
defaultDarkTheme: { type: 'string', nullable: true },
|
||||
clientOptions: {
|
||||
type: 'object', nullable: false,
|
||||
properties: {
|
||||
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
|
||||
showTimelineForVisitor: { type: 'boolean', nullable: false },
|
||||
showActivitiesForVisitor: { type: 'boolean', nullable: false },
|
||||
},
|
||||
},
|
||||
clientOptions: { type: 'object', nullable: false },
|
||||
cacheRemoteFiles: { type: 'boolean' },
|
||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
@@ -225,9 +217,6 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
private metaService: MetaService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
@@ -340,10 +329,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
if (ps.clientOptions !== undefined) {
|
||||
set.clientOptions = {
|
||||
...serverSettings.clientOptions,
|
||||
...ps.clientOptions,
|
||||
};
|
||||
set.clientOptions = ps.clientOptions;
|
||||
}
|
||||
|
||||
if (ps.cacheRemoteFiles !== undefined) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId) {
|
||||
await this.reversiService.matchSpecificUserCancel(me, ps.userId);
|
||||
return;
|
||||
} else {
|
||||
await this.reversiService.matchAnyUserCancel(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.createRoom(me);
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: 'ReversiGameLite' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
my: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
if (ps.my && me) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('game.user1Id = :userId', { userId: me.id })
|
||||
.orWhere('game.user2Id = :userId', { userId: me.id });
|
||||
}));
|
||||
} else {
|
||||
query.andWhere('game.isStarted = TRUE');
|
||||
}
|
||||
|
||||
const games = await query.take(ps.limit).getMany();
|
||||
|
||||
return await this.reversiGameEntityService.packLiteMany(games);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '370e42b0-2a67-4306-9328-51c5f568f110',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.getRoom(ps.roomId);
|
||||
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
await this.mahjongService.joinRoom(room.id, me);
|
||||
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd77df68f-06f3-492b-9078-e6f72f4acf23',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.getRoom(ps.roomId);
|
||||
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
errors: {
|
||||
noSuchGame: {
|
||||
message: 'No such game.',
|
||||
code: 'NO_SUCH_GAME',
|
||||
id: '8fb05624-b525-43dd-90f7-511852bdfeee',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
desynced: { type: 'boolean' },
|
||||
game: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'ReversiGameDetailed',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
crc32: { type: 'string' },
|
||||
},
|
||||
required: ['gameId', 'crc32'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32);
|
||||
if (game) {
|
||||
return {
|
||||
desynced: true,
|
||||
game: await this.reversiGameEntityService.packDetail(game),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
desynced: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { HybridTimelineChannel } from './channels/hybrid-timeline.js';
|
||||
import { LocalTimelineChannel } from './channels/local-timeline.js';
|
||||
import { HomeTimelineChannel } from './channels/home-timeline.js';
|
||||
@@ -23,8 +22,8 @@ import { ChatUserChannel } from './channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './channels/chat-room.js';
|
||||
import { ReversiChannel } from './channels/reversi.js';
|
||||
import { ReversiGameChannel } from './channels/reversi-game.js';
|
||||
import { MahjongRoomChannel } from './channels/mahjong-room.js';
|
||||
import type { ChannelConstructor } from './channel.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelsService {
|
||||
@@ -53,7 +52,6 @@ export class ChannelsService {
|
||||
case 'chatRoom': return ChatRoomChannel;
|
||||
case 'reversi': return ReversiChannel;
|
||||
case 'reversiGame': return ReversiGameChannel;
|
||||
case 'mahjongRoom': return MahjongRoomChannel;
|
||||
|
||||
default:
|
||||
throw new Error(`no such channel: ${name}`);
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import Channel, { type ChannelRequest } from '../channel.js';
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class MahjongRoomChannel extends Channel {
|
||||
public readonly chName = 'mahjongRoom';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:account';
|
||||
private roomId: string | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(REQUEST)
|
||||
request: ChannelRequest,
|
||||
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.roomId = params.roomId as string;
|
||||
|
||||
this.subscriber.on(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMahjongRoomStreamMessage(message: GlobalEvents['mahjongRoom']['payload']) {
|
||||
if (message.type === 'started') {
|
||||
const packed = await this.mahjongService.packRoom(message.body.room, this.user!);
|
||||
this.send('started', {
|
||||
room: packed,
|
||||
});
|
||||
} else if (message.type === 'nextKyoku') {
|
||||
const packed = this.mahjongService.packState(message.body.room, this.user!);
|
||||
this.send('nextKyoku', {
|
||||
state: packed,
|
||||
});
|
||||
} else {
|
||||
this.send(message.type, message.body);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'ready': this.ready(body); break;
|
||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'addAi': this.addAi(); break;
|
||||
case 'leave': this.leaveRoom(); break;
|
||||
case 'confirmNextKyoku': this.confirmNextKyoku(); break;
|
||||
case 'dahai': this.dahai(body.tile, body.riichi); break;
|
||||
case 'tsumoHora': this.tsumoHora(); break;
|
||||
case 'ronHora': this.ronHora(); break;
|
||||
case 'pon': this.pon(); break;
|
||||
case 'cii': this.cii(body.pattern); break;
|
||||
case 'kan': this.kan(); break;
|
||||
case 'ankan': this.ankan(body.tile); break;
|
||||
case 'kakan': this.kakan(body.tile); break;
|
||||
case 'nop': this.nop(); break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: any) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.updateSettings(this.roomId!, this.user, key, value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ready(ready: boolean) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.changeReadyState(this.roomId!, this.user, ready);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async confirmNextKyoku() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.confirmNextKyoku(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async addAi() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.addAi(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async leaveRoom() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.leaveRoom(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async dahai(tile: number, riichi = false) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_dahai(this.roomId!, this.user, tile, riichi);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async tsumoHora() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_tsumoHora(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ronHora() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_ronHora(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async pon() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_pon(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async cii(pattern: string) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_cii(this.roomId!, this.user, pattern);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async kan() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_kan(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ankan(tile: number) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_ankan(this.roomId!, this.user, tile);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async kakan(tile: number) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_kakan(this.roomId!, this.user, tile);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async nop() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_nop(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async claimTimeIsUp() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.checkTimeout(this.roomId!);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
|
||||
}
|
||||
}
|
||||
@@ -78,15 +78,6 @@ services:
|
||||
source: ../../misskey-reversi/package.json
|
||||
target: /misskey/packages/misskey-reversi/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-mahjong/built
|
||||
target: /misskey/packages/misskey-mahjong/built
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-mahjong/package.json
|
||||
target: /misskey/packages/misskey-mahjong/package.json
|
||||
read_only: true
|
||||
|
||||
- type: bind
|
||||
source: ../../../healthcheck.sh
|
||||
target: /misskey/healthcheck.sh
|
||||
|
||||
@@ -144,15 +144,7 @@ export default [
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'any',
|
||||
normal: 'never',
|
||||
component: 'any',
|
||||
},
|
||||
svg: 'any',
|
||||
math: 'any',
|
||||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||
autofix: true,
|
||||
|
||||
@@ -9,16 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.earRight">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"></canvas>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |