Compare commits

...

20 commits

Author SHA1 Message Date
Marie d15e4a85bb
chore: typecheck 2024-01-22 22:13:10 +01:00
Marie 55ea66127a
fix: close button not closing auto expanded notes
Closes #359
2024-01-22 20:11:26 +01:00
Marie fd69a2fbbd
merge: upstream 2024-01-22 19:58:43 +01:00
tamaina 2ee5507d06 fix of #13014 (misskey-js publish) 2024-01-22 15:25:22 +00:00
tamaina 31a39776f5
chore: publish misskey-js automatically (#13014)
* chore: publish @misskey-dev/misskey-js

* remove @misskey-dev/

* ??

* correct version

* version
2024-01-23 00:19:43 +09:00
syuilo 5e307e472d 2024.2.0-beta.3 2024-01-22 18:33:40 +09:00
syuilo e0ad066382 fix lint 2024-01-22 18:32:32 +09:00
syuilo 99fe03bd4d 🎨 2024-01-22 18:31:59 +09:00
おさむのひと 850d38414e
fix: 2024-01-22 10:50時点のdevelopにてCIがコケている (#13060)
* fix: バブルゲームのビルド失敗修正

* fix: api.jsonの定義誤りを修正

* fix: lint.yml(typecheck)

* fix: fix eslint error

* fix: frontend vitest version

* fix: frontend vitest version

* fix:

* fix: cypress

* fix: misskey-js test

* fix: misskey-js tsd(tsdはpakcage.jsonのexportsをサポートしない?)

* fix: conflict

* fix: 間違えて上書きしたところを修正

* fix: 再

* fix: api.json

* fix: api.json

* fix: タイムアウト延長

* Update packages/misskey-js/jest.config.cjs

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2024-01-22 18:01:54 +09:00
syuilo d380ed36de fix lint 2024-01-22 18:00:46 +09:00
syuilo 5c8888d6a8 enhance(reversi): render ogp 2024-01-22 17:59:12 +09:00
syuilo 4af3640bd3 fix lint 2024-01-22 17:44:03 +09:00
syuilo 94e282b612 perf(reversi): improve performance of reversi backend 2024-01-22 15:41:29 +09:00
syuilo 259992c65f enhance(reversi): some tweaks 2024-01-22 12:03:32 +09:00
syuilo 67f6157d42 2024.2.0-beta.2 2024-01-22 09:30:00 +09:00
syuilo 0cfeb42786
New Crowdin updates (#13056)
* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Danish)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Hungarian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Croatian)

* New translations ja-jp.yml (Uyghur)

* New translations ja-jp.yml (Lojban)

* New translations ja-jp.yml (Sinhala)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Kannada)

* New translations ja-jp.yml (Haitian Creole)

* New translations ja-jp.yml (Kabyle)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)
2024-01-22 09:29:26 +09:00
syuilo a431dde537 refactor(reversi): refactoring of reversi backend 2024-01-22 09:29:06 +09:00
かっこかり 4f95b8d9d2
fix(frontend/pizzax): デフォルト値が適用できないことがあるのを修正 (#13057)
* fix(frontend/pizzax): デフォルト値が適用できないことがあるのを修正

* fix

* いらんプロパティをけす
2024-01-22 09:20:56 +09:00
syuilo 9eb0468cd2 2024.2.0-beta.1 2024-01-22 09:14:45 +09:00
syuilo 1a01a85182 perf(reversi): improve performance of reversi backend 2024-01-22 08:39:38 +09:00
76 changed files with 663 additions and 747 deletions

View file

@ -161,11 +161,13 @@ describe('After user signed in', () => {
}); });
it('successfully loads', () => { it('successfully loads', () => {
cy.get('[data-cy-user-setup-continue]').should('be.visible'); // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).should('be.visible');
}); });
it('account setup wizard', () => { it('account setup wizard', () => {
cy.get('[data-cy-user-setup-continue]').click(); // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).click();
cy.get('[data-cy-user-setup-user-name] input').type('ありす'); cy.get('[data-cy-user-setup-user-name] input').type('ありす');
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
@ -202,7 +204,8 @@ describe('After user setup', () => {
cy.login('alice', 'alice1234'); cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード // アカウント初期設定ウィザード
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click(); // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
cy.get('[data-cy-modal-dialog-ok]').click(); cy.get('[data-cy-modal-dialog-ok]').click();
}); });

View file

@ -1567,3 +1567,4 @@ _moderationLogTypes:
createInvitation: "ولِّد دعوة" createInvitation: "ولِّد دعوة"
_reversi: _reversi:
total: "المجموع" total: "المجموع"

View file

@ -1346,3 +1346,4 @@ _moderationLogTypes:
resetPassword: "পাসওয়ার্ড রিসেট করুন" resetPassword: "পাসওয়ার্ড রিসেট করুন"
_reversi: _reversi:
total: "মোট" total: "মোট"

View file

@ -1276,3 +1276,4 @@ _moderationLogTypes:
resetPassword: "Restableix la contrasenya" resetPassword: "Restableix la contrasenya"
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -2022,3 +2022,4 @@ _moderationLogTypes:
createInvitation: "Vygenerovat pozvánku" createInvitation: "Vygenerovat pozvánku"
_reversi: _reversi:
total: "Celkem" total: "Celkem"

View file

@ -1,2 +1,3 @@
--- ---
_lang_: "Dansk" _lang_: "Dansk"

View file

@ -2247,3 +2247,4 @@ _externalResourceInstaller:
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
_reversi: _reversi:
total: "Gesamt" total: "Gesamt"

View file

@ -398,3 +398,4 @@ _moderationLogTypes:
suspend: "Αποβολή" suspend: "Αποβολή"
_reversi: _reversi:
total: "Σύνολο" total: "Σύνολο"

View file

@ -2561,3 +2561,4 @@ _dataSaver:
description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data."
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -2429,3 +2429,4 @@ _dataSaver:
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -2085,3 +2085,4 @@ _dataSaver:
description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données."
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -3,3 +3,4 @@ _lang_: "japanski"
ok: "OK" ok: "OK"
gotIt: "Razumijem" gotIt: "Razumijem"
cancel: "otkazati" cancel: "otkazati"

View file

@ -16,3 +16,4 @@ _2fa:
renewTOTPCancel: "Sispann" renewTOTPCancel: "Sispann"
_widgets: _widgets:
profile: "pwofil" profile: "pwofil"

View file

@ -102,3 +102,4 @@ _deck:
_columns: _columns:
notifications: "Értesítések" notifications: "Értesítések"
tl: "Idővonal" tl: "Idővonal"

View file

@ -2321,3 +2321,4 @@ _dataSaver:
description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data."
_reversi: _reversi:
total: "Jumlah" total: "Jumlah"

View file

@ -2361,3 +2361,4 @@ _dataSaver:
description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato."
_reversi: _reversi:
total: "Totale" total: "Totale"

View file

@ -2414,3 +2414,4 @@ _dataSaver:
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
_reversi: _reversi:
total: "合計" total: "合計"

View file

@ -1,3 +1,4 @@
--- ---
_lang_: "la .lojban." _lang_: "la .lojban."
headlineMisskey: "lo se tcana noi jorne fi loi notci" headlineMisskey: "lo se tcana noi jorne fi loi notci"

View file

@ -104,3 +104,4 @@ _deck:
_columns: _columns:
notifications: "Ilɣuyen" notifications: "Ilɣuyen"
list: "Tibdarin" list: "Tibdarin"

View file

@ -84,3 +84,4 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು" notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು" tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ" mentions: "ಹೆಸರಿಸಿದ"

View file

@ -726,3 +726,4 @@ _moderationLogTypes:
resolveAbuseReport: "신고 해겔하기" resolveAbuseReport: "신고 해겔하기"
_reversi: _reversi:
total: "합계" total: "합계"

View file

@ -2415,3 +2415,4 @@ _dataSaver:
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다." description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
_reversi: _reversi:
total: "합계" total: "합계"

View file

@ -466,3 +466,4 @@ _webhookSettings:
name: "ຊື່" name: "ຊື່"
_moderationLogTypes: _moderationLogTypes:
suspend: "ລະງັບ" suspend: "ລະງັບ"

View file

@ -497,3 +497,4 @@ _webhookSettings:
_moderationLogTypes: _moderationLogTypes:
suspend: "Opschorten" suspend: "Opschorten"
resetPassword: "Wachtwoord terugzetten" resetPassword: "Wachtwoord terugzetten"

View file

@ -720,3 +720,4 @@ _webhookSettings:
name: "Navn" name: "Navn"
_moderationLogTypes: _moderationLogTypes:
suspend: "Suspender" suspend: "Suspender"

View file

@ -1399,3 +1399,4 @@ _moderationLogTypes:
resetPassword: "Zresetuj hasło" resetPassword: "Zresetuj hasło"
_reversi: _reversi:
total: "Łącznie" total: "Łącznie"

View file

@ -1500,3 +1500,4 @@ _moderationLogTypes:
resetPassword: "Redefinir senha" resetPassword: "Redefinir senha"
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -729,3 +729,4 @@ _moderationLogTypes:
resetPassword: "Resetează parola" resetPassword: "Resetează parola"
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -1972,3 +1972,4 @@ _moderationLogTypes:
resetPassword: "Сброс пароля:" resetPassword: "Сброс пароля:"
_reversi: _reversi:
total: "Всего" total: "Всего"

View file

@ -1 +1,2 @@
--- ---

View file

@ -1447,3 +1447,4 @@ _moderationLogTypes:
resetPassword: "Resetovať heslo" resetPassword: "Resetovať heslo"
_reversi: _reversi:
total: "Celkom" total: "Celkom"

View file

@ -576,3 +576,4 @@ _webhookSettings:
_moderationLogTypes: _moderationLogTypes:
suspend: "Suspendera" suspend: "Suspendera"
resetPassword: "Återställ Lösenord" resetPassword: "Återställ Lösenord"

View file

@ -2440,3 +2440,4 @@ _dataSaver:
description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้"
_reversi: _reversi:
total: "รวมทั้งหมด" total: "รวมทั้งหมด"

View file

@ -455,3 +455,4 @@ _deck:
_moderationLogTypes: _moderationLogTypes:
suspend: "askıya al" suspend: "askıya al"
resetPassword: "Şifre sıfırlama" resetPassword: "Şifre sıfırlama"

View file

@ -17,3 +17,4 @@ _2fa:
renewTOTPCancel: "ئۇنى توختىتىڭ" renewTOTPCancel: "ئۇنى توختىتىڭ"
_widgets: _widgets:
profile: "profile" profile: "profile"

View file

@ -1622,3 +1622,4 @@ _moderationLogTypes:
resetPassword: "Скинути пароль" resetPassword: "Скинути пароль"
_reversi: _reversi:
total: "Всього" total: "Всього"

View file

@ -1090,3 +1090,4 @@ _moderationLogTypes:
resetPassword: "Parolni tiklash" resetPassword: "Parolni tiklash"
_reversi: _reversi:
total: "Jami" total: "Jami"

View file

@ -1852,3 +1852,4 @@ _moderationLogTypes:
resetPassword: "Đặt lại mật khẩu" resetPassword: "Đặt lại mật khẩu"
_reversi: _reversi:
total: "Tổng cộng" total: "Tổng cộng"

View file

@ -1200,6 +1200,8 @@ replaying: "重播中"
ranking: "排行榜" ranking: "排行榜"
lastNDays: "最近 {n} 天" lastNDays: "最近 {n} 天"
backToTitle: "返回标题" backToTitle: "返回标题"
hemisphere: "居住地区"
withSensitive: "显示包含敏感媒体的帖子"
enableHorizontalSwipe: "滑动切换标签页" enableHorizontalSwipe: "滑动切换标签页"
_bubbleGame: _bubbleGame:
howToPlay: "游戏说明" howToPlay: "游戏说明"
@ -2427,9 +2429,14 @@ _dataSaver:
_code: _code:
title: "代码高亮" title: "代码高亮"
description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。"
_hemisphere:
N: "北半球"
S: "南半球"
caption: "在某些客户端设置中用来确定季节"
_reversi: _reversi:
reversi: "黑白棋" reversi: "黑白棋"
total: "总计" total: "总计"
_offlineScreen: _offlineScreen:
title: "离线——无法连接到服务器" title: "离线——无法连接到服务器"
header: "无法连接到服务器" header: "无法连接到服务器"

View file

@ -1202,6 +1202,9 @@ replaying: "重播中"
ranking: "排行榜" ranking: "排行榜"
lastNDays: "過去 {n} 天" lastNDays: "過去 {n} 天"
backToTitle: "回到遊戲標題頁" backToTitle: "回到遊戲標題頁"
hemisphere: "您居住的地區"
withSensitive: "顯示包含敏感檔案的貼文"
userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文"
enableHorizontalSwipe: "滑動切換時間軸" enableHorizontalSwipe: "滑動切換時間軸"
_bubbleGame: _bubbleGame:
howToPlay: "玩法說明" howToPlay: "玩法說明"
@ -2438,5 +2441,48 @@ _dataSaver:
_code: _code:
title: "程式碼突出顯示" title: "程式碼突出顯示"
description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。" description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。"
_hemisphere:
N: "北半球"
S: "南半球"
caption: "在某些客戶端的設定中,用於判斷季節。"
_reversi: _reversi:
reversi: "黑白棋"
gameSettings: "對弈設定"
chooseBoard: "選擇棋盤"
blackOrWhite: "先手/後手"
blackIs: "{name} 為黑棋(先攻)"
rules: "規則"
thisGameIsStartedSoon: "對弈即將開始"
waitingForOther: "等待對手準備就緒"
waitingForMe: "等待您準備就緒"
waitingBoth: "請準備"
ready: "準備就緒"
cancelReady: "重新準備"
opponentTurn: "對手的回合"
myTurn: "您的回合"
turnOf: "{name} 的回合"
pastTurnOf: "{name} 的回合"
surrender: "認輸"
surrendered: "對手認輸"
timeout: "時間到"
drawn: "平手"
won: "{name} 獲勝"
black: "黑"
white: "白"
total: "合計" total: "合計"
turnCount: "{count} 回合"
myGames: "我的對弈"
allGames: "所有對弈"
ended: ""
playing: "正在對弈"
isLlotheo: "子較少的一方為勝(顛倒規則)"
loopedMap: "循環棋盤"
canPutEverywhere: "隨意置放模式"
timeLimitForEachTurn: "每回合的時間限制"
freeMatch: "自由對戰"
lookingForPlayer: "正在搜尋對手"
gameCanceled: "對弈已被取消"
_offlineScreen:
title: "離線-無法連接伺服器"
header: "無法連接伺服器"

View file

@ -1,6 +1,6 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2024.1.0.beta2", "version": "2024.2.0-beta1",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",
@ -56,8 +56,8 @@
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.19.0", "@typescript-eslint/parser": "6.18.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.6.3", "cypress": "13.6.3",
"eslint": "8.56.0", "eslint": "8.56.0",

View file

@ -8,7 +8,7 @@
}, },
"scripts": { "scripts": {
"start": "node ./built/boot/entry.js", "start": "node ./built/boot/entry.js",
"start:test": "NODE_ENV=test node ./built/boot/entry.js", "start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js",
"check:connect": "node ./check_connect.js", "check:connect": "node ./check_connect.js",
@ -31,7 +31,7 @@
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
"generate-api-json": "node ./generate_api_json.js" "generate-api-json": "pnpm build && node ./generate_api_json.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",

View file

@ -55,23 +55,29 @@ export class AntennaService implements OnApplicationShutdown {
const { type, body } = obj.message as GlobalEvents['internal']['payload']; const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) { switch (type) {
case 'antennaCreated': case 'antennaCreated':
this.antennas.push({ this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
lastUsedAt: new Date(body.lastUsedAt), lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
}); });
break; break;
case 'antennaUpdated': { case 'antennaUpdated': {
const idx = this.antennas.findIndex(a => a.id === body.id); const idx = this.antennas.findIndex(a => a.id === body.id);
if (idx >= 0) { if (idx >= 0) {
this.antennas[idx] = { this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
lastUsedAt: new Date(body.lastUsedAt), lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
}; };
} else { } else {
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり // サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
this.antennas.push({ this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
lastUsedAt: new Date(body.lastUsedAt), lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
}); });
} }
} }

View file

@ -51,7 +51,10 @@ export class MetaService implements OnApplicationShutdown {
const { type, body } = obj.message as GlobalEvents['internal']['payload']; const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) { switch (type) {
case 'metaUpdated': { case 'metaUpdated': {
this.cache = body; this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
proxyAccount: null, // joinなカラムは通常取ってこないので
};
break; break;
} }
default: default:

View file

@ -12,18 +12,14 @@ import { IsNull } from 'typeorm';
import type { import type {
MiReversiGame, MiReversiGame,
ReversiGamesRepository, ReversiGamesRepository,
UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Packed } from '@/misc/json-schema.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { Serialized } from '@/types.js'; import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
@ -58,7 +54,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis @bindThis
private async cacheGame(game: MiReversiGame) { private async cacheGame(game: MiReversiGame) {
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game)); await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game));
} }
@bindThis @bindThis
@ -66,6 +62,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
await this.redisClient.del(`reversi:game:cache:${gameId}`); await this.redisClient.del(`reversi:game:cache:${gameId}`);
} }
@bindThis
private getBakeProps(game: MiReversiGame) {
return {
startedAt: game.startedAt,
endedAt: game.endedAt,
// ゲームの途中からユーザーが変わることは無いので
//user1Id: game.user1Id,
//user2Id: game.user2Id,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
black: game.black,
isStarted: game.isStarted,
isEnded: game.isEnded,
winnerId: game.winnerId,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn,
logs: game.logs,
map: game.map,
bw: game.bw,
crc32: game.crc32,
} satisfies Partial<MiReversiGame>;
}
@bindThis @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) { if (targetUser.id === me.id) {
@ -81,23 +104,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (invitations.includes(targetUser.id)) { if (invitations.includes(targetUser.id)) {
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
const game = await this.reversiGamesRepository.insert({ const game = await this.matched(targetUser.id, me.id);
id: this.idService.gen(),
user1Id: targetUser.id,
user2Id: me.id,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
map: Reversi.maps.eighteight.data,
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
return game; return game;
} else { } else {
@ -124,23 +131,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const invitorId = invitations[Math.floor(Math.random() * invitations.length)]; const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId); await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
const game = await this.reversiGamesRepository.insert({ const game = await this.matched(invitorId, me.id);
id: this.idService.gen(),
user1Id: invitorId,
user2Id: me.id,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
map: Reversi.maps.eighteight.data,
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
return game; return game;
} }
@ -160,23 +151,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
const game = await this.reversiGamesRepository.insert({ const game = await this.matched(matchedUserId, me.id);
id: this.idService.gen(),
user1Id: matchedUserId,
user2Id: me.id,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
map: Reversi.maps.eighteight.data,
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
return game; return game;
} else { } else {
@ -204,14 +179,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
let isBothReady = false; let isBothReady = false;
if (game.user1Id === user.id) { if (game.user1Id === user.id) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
user1Ready: ready, user1Ready: ready,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
@ -221,14 +192,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (ready && updatedGame.user2Ready) isBothReady = true; if (ready && updatedGame.user2Ready) isBothReady = true;
} else if (game.user2Id === user.id) { } else if (game.user2Id === user.id) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
user2Ready: ready, user2Ready: ready,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
@ -253,6 +220,32 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
} }
@bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
const game = await this.reversiGamesRepository.insert({
id: this.idService.gen(),
user1Id: parentId,
user2Id: childId,
user1Ready: false,
user2Ready: false,
isStarted: false,
isEnded: false,
logs: [],
map: Reversi.maps.eighteight.data,
bw: 'random',
isLlotheo: false,
}).then(x => this.reversiGamesRepository.findOneOrFail({
where: { id: x.identifiers[0].id },
relations: ['user1', 'user2'],
}));
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game);
this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
return game;
}
@bindThis @bindThis
private async startGame(game: MiReversiGame) { private async startGame(game: MiReversiGame) {
let bw: number; let bw: number;
@ -262,63 +255,44 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw = parseInt(game.bw, 10); bw = parseInt(game.bw, 10);
} }
function getRandomMap() {
const mapCount = Object.entries(Reversi.maps).length;
const rnd = Math.floor(Math.random() * mapCount);
return Object.values(Reversi.maps)[rnd].data;
}
const map = game.map != null ? game.map : getRandomMap();
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({ .set({
...this.getBakeProps(game),
startedAt: new Date(), startedAt: new Date(),
isStarted: true, isStarted: true,
black: bw, black: bw,
map: map, map: game.map,
crc32, crc32,
}) })
.where('id = :id', { id: game.id }) .where('id = :id', { id: game.id })
.returning('*') .returning('*')
.execute() .execute()
.then((response) => response.raw[0]); .then((response) => response.raw[0]);
// キャッシュ効率化のためにユーザー情報は再利用
updatedGame.user1 = game.user1;
updatedGame.user2 = game.user2;
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const engine = new Reversi.Game(map, { const engine = new Reversi.Game(updatedGame.map, {
isLlotheo: game.isLlotheo, isLlotheo: updatedGame.isLlotheo,
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: updatedGame.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: updatedGame.loopedBoard,
}); });
if (engine.isEnded) { if (engine.isEnded) {
let winner; let winnerId;
if (engine.winner === true) { if (engine.winner === true) {
winner = bw === 1 ? game.user1Id : game.user2Id; winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id;
} else if (engine.winner === false) { } else if (engine.winner === false) {
winner = bw === 1 ? game.user2Id : game.user1Id; winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id;
} else { } else {
winner = null; winnerId = null;
} }
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(updatedGame, winnerId, null);
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winner,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winner,
game: await this.reversiGameEntityService.packDetail(game.id),
});
return; return;
} }
@ -327,7 +301,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, ''); this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
this.globalEventService.publishReversiGameStream(game.id, 'started', { this.globalEventService.publishReversiGameStream(game.id, 'started', {
game: await this.reversiGameEntityService.packDetail(game.id), game: await this.reversiGameEntityService.packDetail(updatedGame),
});
}
@bindThis
private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({
...this.getBakeProps(game),
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
// キャッシュ効率化のためにユーザー情報は再利用
updatedGame.user1 = game.user1;
updatedGame.user2 = game.user2;
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(updatedGame),
}); });
} }
@ -354,14 +354,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
// TODO: より厳格なバリデーション // TODO: より厳格なバリデーション
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
[key]: value, [key]: value,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
@ -397,17 +393,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
engine.putStone(pos); engine.putStone(pos);
let winner;
if (engine.isEnded) {
if (engine.winner === true) {
winner = game.black === 1 ? game.user1Id : game.user2Id;
} else if (engine.winner === false) {
winner = game.black === 1 ? game.user2Id : game.user1Id;
} else {
winner = null;
}
}
const logs = Reversi.Serializer.deserializeLogs(game.logs); const logs = Reversi.Serializer.deserializeLogs(game.logs);
const log = { const log = {
@ -423,17 +408,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
crc32, crc32,
isEnded: engine.isEnded,
winnerId: winner,
logs: serializeLogs, logs: serializeLogs,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'log', { this.globalEventService.publishReversiGameStream(game.id, 'log', {
@ -442,10 +421,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}); });
if (engine.isEnded) { if (engine.isEnded) {
this.globalEventService.publishReversiGameStream(game.id, 'ended', { let winnerId;
winnerId: winner ?? null, if (engine.winner === true) {
game: await this.reversiGameEntityService.packDetail(game.id), winnerId = game.black === 1 ? game.user1Id : game.user2Id;
}); } else if (engine.winner === false) {
winnerId = game.black === 1 ? game.user2Id : game.user1Id;
} else {
winnerId = null;
}
await this.endGame(updatedGame, winnerId, null);
} else { } else {
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, ''); this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
} }
@ -460,23 +445,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(game, winnerId, 'surrender');
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
surrenderedUserId: user.id,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(game.id),
});
} }
@bindThis @bindThis
@ -500,23 +469,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (timer === 0) { if (timer === 0) {
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id); const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(game, winnerId, 'timeout');
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(game.id),
});
} }
} }
@ -539,14 +492,38 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> { public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
const cached = await this.redisClient.get(`reversi:game:cache:${id}`); const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
if (cached != null) { if (cached != null) {
// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>; const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
return { return {
...parsed, ...parsed,
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null, startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null, endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
user1: parsed.user1 != null ? {
...parsed.user1,
avatar: null,
banner: null,
background: null,
updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null,
lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
} : null,
user2: parsed.user2 != null ? {
...parsed.user2,
avatar: null,
banner: null,
background: null,
updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null,
lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
} : null,
}; };
} else { } else {
const game = await this.reversiGamesRepository.findOneBy({ id }); const game = await this.reversiGamesRepository.findOne({
where: { id },
relations: ['user1', 'user2'],
});
if (game == null) return null; if (game == null) return null;
this.cacheGame(game); this.cacheGame(game);

View file

@ -181,9 +181,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
case 'userRoleAssigned': { case 'userRoleAssigned': {
const cached = this.roleAssignmentByUserIdCache.get(body.userId); const cached = this.roleAssignmentByUserIdCache.get(body.userId);
if (cached) { if (cached) {
cached.push({ cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
user: null, // joinなカラムは通常取ってこないので
role: null, // joinなカラムは通常取ってこないので
}); });
} }
break; break;

View file

@ -49,9 +49,10 @@ export class WebhookService implements OnApplicationShutdown {
switch (type) { switch (type) {
case 'webhookCreated': case 'webhookCreated':
if (body.active) { if (body.active) {
this.webhooks.push({ this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
}); });
} }
break; break;
@ -59,14 +60,16 @@ export class WebhookService implements OnApplicationShutdown {
if (body.active) { if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id); const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) { if (i > -1) {
this.webhooks[i] = { this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
}; };
} else { } else {
this.webhooks.push({ this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
}); });
} }
} else { } else {

View file

@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js'; import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiReversiGame } from '@/models/ReversiGame.js'; import type { MiReversiGame } from '@/models/ReversiGame.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -29,10 +28,14 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public async packDetail( public async packDetail(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
me?: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'ReversiGameDetailed'>> { ): Promise<Packed<'ReversiGameDetailed'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([
this.userEntityService.pack(game.user1 ?? game.user1Id),
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
return await awaitAll({ return await awaitAll({
id: game.id, id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(), createdAt: this.idService.parse(game.id).date.toISOString(),
@ -46,10 +49,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready, user2Ready: game.user2Ready,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me), user1: users[0],
user2: this.userEntityService.pack(game.user2Id, me), user2: users[1],
winnerId: game.winnerId, winnerId: game.winnerId,
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -66,18 +69,21 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public packDetailMany( public packDetailMany(
xs: MiReversiGame[], xs: MiReversiGame[],
me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(xs.map(x => this.packDetail(x, me))); return Promise.all(xs.map(x => this.packDetail(x)));
} }
@bindThis @bindThis
public async packLite( public async packLite(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
me?: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'ReversiGameLite'>> { ): Promise<Packed<'ReversiGameLite'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([
this.userEntityService.pack(game.user1 ?? game.user1Id),
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
return await awaitAll({ return await awaitAll({
id: game.id, id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(), createdAt: this.idService.parse(game.id).date.toISOString(),
@ -85,16 +91,12 @@ export class ReversiGameEntityService {
endedAt: game.endedAt && game.endedAt.toISOString(), endedAt: game.endedAt && game.endedAt.toISOString(),
isStarted: game.isStarted, isStarted: game.isStarted,
isEnded: game.isEnded, isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me), user1: users[0],
user2: this.userEntityService.pack(game.user2Id, me), user2: users[1],
winnerId: game.winnerId, winnerId: game.winnerId,
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -109,9 +111,8 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public packLiteMany( public packLiteMany(
xs: MiReversiGame[], xs: MiReversiGame[],
me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(xs.map(x => this.packLite(x, me))); return Promise.all(xs.map(x => this.packLite(x)));
} }
} }

View file

@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
form1: {
type: 'any',
optional: false, nullable: true,
},
form2: {
type: 'any',
optional: false, nullable: true,
},
user1Ready: {
type: 'boolean',
optional: false, nullable: false,
},
user2Ready: {
type: 'boolean',
optional: false, nullable: false,
},
user1Id: { user1Id: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
@ -149,11 +133,11 @@ export const packedReversiGameDetailedSchema = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
form1: { form1: {
type: 'any', type: 'object',
optional: false, nullable: true, optional: false, nullable: true,
}, },
form2: { form2: {
type: 'any', type: 'object',
optional: false, nullable: true, optional: false, nullable: true,
}, },
user1Ready: { user1Ready: {

View file

@ -43,7 +43,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
.andWhere('game.isStarted = TRUE'); .andWhere('game.isStarted = TRUE')
.innerJoinAndSelect('game.user1', 'user1')
.innerJoinAndSelect('game.user2', 'user2');
if (ps.my && me) { if (ps.my && me) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
@ -55,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const games = await query.take(ps.limit).getMany(); const games = await query.take(ps.limit).getMany();
return await this.reversiGameEntityService.packLiteMany(games, me); return await this.reversiGameEntityService.packLiteMany(games);
}); });
} }
} }

View file

@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (game == null) return; if (game == null) return;
return await this.reversiGameEntityService.packDetail(game, me); return await this.reversiGameEntityService.packDetail(game);
}); });
} }
} }

View file

@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchGame); throw new ApiError(meta.errors.noSuchGame);
} }
return await this.reversiGameEntityService.packDetail(game, me); return await this.reversiGameEntityService.packDetail(game);
}); });
} }
} }

View file

@ -42,7 +42,7 @@ class ReversiGameChannel extends Channel {
case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'cancel': this.cancelGame(); break; case 'cancel': this.cancelGame(); break;
case 'putStone': this.putStone(body.pos, body.id); break; case 'putStone': this.putStone(body.pos, body.id); break;
case 'checkState': this.checkState(body.crc32); break; case 'resync': this.resync(body.crc32); break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
} }
} }
@ -76,12 +76,10 @@ class ReversiGameChannel extends Channel {
} }
@bindThis @bindThis
private async checkState(crc32: string | number) { private async resync(crc32: string | number) {
if (crc32 != null) return;
const game = await this.reversiService.checkCrc(this.gameId!, crc32); const game = await this.reversiService.checkCrc(this.gameId!, crc32);
if (game) { if (game) {
this.send('rescue', game); this.send('resynced', game);
} }
} }

View file

@ -31,12 +31,13 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -83,6 +84,9 @@ export class ClientServerService {
@Inject(DI.flashsRepository) @Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository, private flashsRepository: FlashsRepository,
@Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -90,6 +94,7 @@ export class ClientServerService {
private galleryPostEntityService: GalleryPostEntityService, private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
private metaService: MetaService, private metaService: MetaService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
@ -704,6 +709,25 @@ export class ClientServerService {
return await renderBase(reply); return await renderBase(reply);
} }
}); });
// Reversi game
fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => {
const game = await this.reversiGamesRepository.findOneBy({
id: request.params.game,
});
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', {
game: _game,
...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
}
});
//#endregion //#endregion
fastify.get('/_info_card_', async (request, reply) => { fastify.get('/_info_card_', async (request, reply) => {

View file

@ -0,0 +1,20 @@
extends ./base
block vars
- const user1 = game.user1;
- const user2 = game.user2;
- const title = `${user1.username} vs ${user2.username}`;
- const url = `${config.url}/reversi/g/${game.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
meta(property='og:url' content= url)
meta(property='twitter:card' content='summary')

View file

@ -283,6 +283,10 @@ export type Serialized<T> = {
? (string | null) ? (string | null)
: T[K] extends Record<string, any> : T[K] extends Record<string, any>
? Serialized<T[K]> ? Serialized<T[K]>
: T[K] extends (Record<string, any> | null)
? (Serialized<T[K]> | null)
: T[K] extends (Record<string, any> | undefined)
? (Serialized<T[K]> | undefined)
: T[K]; : T[K];
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View file

@ -45,6 +45,7 @@
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"cropperjs": "2.0.0-beta.4", "cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"defu": "^6.1.4",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
@ -54,9 +55,9 @@
"json5": "2.2.3", "json5": "2.2.3",
"katex": "0.16.9", "katex": "0.16.9",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"misskey-bubble-game": "workspace:*",
"photoswipe": "5.4.3", "photoswipe": "5.4.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"rollup": "4.9.6", "rollup": "4.9.6",
@ -112,7 +113,7 @@
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "6.18.1",
"@vitest/coverage-v8": "1.2.1", "@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.4.15", "@vue/runtime-core": "3.4.15",
"acorn": "8.11.3", "acorn": "8.11.3",
"cross-env": "7.0.3", "cross-env": "7.0.3",
@ -134,7 +135,7 @@
"storybook": "7.6.10", "storybook": "7.6.10",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "1.2.1", "vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.4.0", "vue-eslint-parser": "9.4.0",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"

View file

@ -293,7 +293,7 @@ const showContent = ref(defaultStore.state.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null); const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong); const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); const muted = ref(checkMute(appearNote.value, $i?.mutedWords));

View file

@ -294,7 +294,7 @@ const showContent = ref(defaultStore.state.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null); const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong); const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); const muted = ref(checkMute(appearNote.value, $i?.mutedWords));

View file

@ -163,7 +163,7 @@ const $i = signinRequired();
const props = defineProps<{ const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed; game: Misskey.entities.ReversiGameDetailed;
connection: Misskey.ChannelConnection; connection?: Misskey.ChannelConnection | null;
}>(); }>();
const showBoardLabels = ref<boolean>(false); const showBoardLabels = ref<boolean>(false);
@ -240,10 +240,10 @@ watch(logPos, (v) => {
if (game.value.isStarted && !game.value.isEnded) { if (game.value.isStarted && !game.value.isEnded) {
useInterval(() => { useInterval(() => {
if (game.value.isEnded) return; if (game.value.isEnded || props.connection == null) return;
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
if (_DEV_) console.log('crc32', crc32); if (_DEV_) console.log('crc32', crc32);
props.connection.send('checkState', { props.connection.send('resync', {
crc32: crc32, crc32: crc32,
}); });
}, 10000, { immediate: false, afterMounted: true }); }, 10000, { immediate: false, afterMounted: true });
@ -267,7 +267,7 @@ function putStone(pos) {
}); });
const id = Math.random().toString(36).slice(2); const id = Math.random().toString(36).slice(2);
props.connection.send('putStone', { props.connection!.send('putStone', {
pos: pos, pos: pos,
id, id,
}); });
@ -283,7 +283,8 @@ const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn); const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
const TIMER_INTERVAL_SEC = 3; const TIMER_INTERVAL_SEC = 3;
useInterval(() => { if (!props.game.isEnded) {
useInterval(() => {
if (myTurnTimerRmain.value > 0) { if (myTurnTimerRmain.value > 0) {
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC); myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
} }
@ -293,12 +294,13 @@ useInterval(() => {
if (iAmPlayer.value) { if (iAmPlayer.value) {
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
props.connection.send('claimTimeIsUp', {}); props.connection!.send('claimTimeIsUp', {});
} }
} }
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
}
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
game.value.logs = Reversi.Serializer.serializeLogs([ game.value.logs = Reversi.Serializer.serializeLogs([
...Reversi.Serializer.deserializeLogs(game.value.logs), ...Reversi.Serializer.deserializeLogs(game.value.logs),
log, log,
@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
if (log.id == null || !appliedOps.includes(log.id)) { if (log.id == null || !appliedOps.includes(log.id)) {
switch (log.operation) { switch (log.operation) {
case 'put': { case 'put': {
sound.playUrl('/client-assets/reversi/put.mp3', {
volume: 1,
playbackRate: 1,
});
if (log.player !== engine.value.turn) { // = desync
const _game = await misskeyApi('reversi/show-game', {
gameId: props.game.id,
});
restoreGame(_game);
return;
}
engine.value.putStone(log.pos); engine.value.putStone(log.pos);
triggerRef(engine); triggerRef(engine);
myTurnTimerRmain.value = game.value.timeLimitForEachTurn; myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
opTurnTimerRmain.value = game.value.timeLimitForEachTurn; opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
sound.playUrl('/client-assets/reversi/put.mp3', {
volume: 1,
playbackRate: 1,
});
checkEnd(); checkEnd();
break; break;
} }
@ -366,9 +376,7 @@ function checkEnd() {
} }
} }
function onStreamRescue(_game) { function restoreGame(_game) {
console.log('rescue');
game.value = deepClone(_game); game.value = deepClone(_game);
engine.value = Reversi.Serializer.restoreGame({ engine.value = Reversi.Serializer.restoreGame({
@ -384,6 +392,12 @@ function onStreamRescue(_game) {
checkEnd(); checkEnd();
} }
function onStreamResynced(_game) {
console.log('resynced');
restoreGame(_game);
}
async function surrender() { async function surrender() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
@ -434,27 +448,35 @@ function share() {
} }
onMounted(() => { onMounted(() => {
if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('log', onStreamLog);
props.connection.on('rescue', onStreamRescue); props.connection.on('resynced', onStreamResynced);
props.connection.on('ended', onStreamEnded); props.connection.on('ended', onStreamEnded);
}
}); });
onActivated(() => { onActivated(() => {
if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('log', onStreamLog);
props.connection.on('rescue', onStreamRescue); props.connection.on('resynced', onStreamResynced);
props.connection.on('ended', onStreamEnded); props.connection.on('ended', onStreamEnded);
}
}); });
onDeactivated(() => { onDeactivated(() => {
if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('log', onStreamLog);
props.connection.off('rescue', onStreamRescue); props.connection.off('resynced', onStreamResynced);
props.connection.off('ended', onStreamEnded); props.connection.off('ended', onStreamEnded);
}
}); });
onUnmounted(() => { onUnmounted(() => {
if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('log', onStreamLog);
props.connection.off('rescue', onStreamRescue); props.connection.off('resynced', onStreamResynced);
props.connection.off('ended', onStreamEnded); props.connection.off('ended', onStreamEnded);
}
}); });
</script> </script>

View file

@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div v-if="game == null || connection == null"><MkLoading/></div> <div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection"/> <GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
<GameBoard v-else :game="game" :connection="connection"/> <GameBoard v-else :game="game" :connection="connection"/>
</template> </template>
@ -47,6 +47,7 @@ async function fetchGame() {
if (connection.value) { if (connection.value) {
connection.value.dispose(); connection.value.dispose();
} }
if (!game.value.isEnded) {
connection.value = useStream().useChannel('reversiGame', { connection.value = useStream().useChannel('reversiGame', {
gameId: game.value.id, gameId: game.value.id,
}); });
@ -64,6 +65,7 @@ async function fetchGame() {
router.push('/reversi'); router.push('/reversi');
} }
}); });
}
} }
onMounted(() => { onMounted(() => {

View file

@ -73,9 +73,8 @@ const src = computed({
set: (x) => saveSrc(x), set: (x) => saveSrc(x),
}); });
const withRenotes = computed({ const withRenotes = computed({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)), set: (x: boolean) => saveTlFilter('withRenotes', x),
set: (x) => saveTlFilter('withRenotes', x),
}); });
const withReplies = computed({ const withReplies = computed({
get: () => { get: () => {
@ -83,11 +82,10 @@ const withReplies = computed({
if (['local', 'social'].includes(src.value) && onlyFiles.value) { if (['local', 'social'].includes(src.value) && onlyFiles.value) {
return false; return false;
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return defaultStore.reactiveState.tl.value.filter.withReplies;
return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
} }
}, },
set: (x) => saveTlFilter('withReplies', x), set: (x: boolean) => saveTlFilter('withReplies', x),
}); });
const withBots = computed({ const withBots = computed({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -99,16 +97,14 @@ const onlyFiles = computed({
if (['local', 'social'].includes(src.value) && withReplies.value) { if (['local', 'social'].includes(src.value) && withReplies.value) {
return false; return false;
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return defaultStore.reactiveState.tl.value.filter.onlyFiles;
return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
} }
}, },
set: (x) => saveTlFilter('onlyFiles', x), set: (x: boolean) => saveTlFilter('onlyFiles', x),
}); });
const withSensitive = computed({ const withSensitive = computed({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)), set: (x: boolean) => {
set: (x) => {
saveTlFilter('withSensitive', x); saveTlFilter('withSensitive', x);
// //

View file

@ -7,6 +7,7 @@
import { onUnmounted, Ref, ref, watch } from 'vue'; import { onUnmounted, Ref, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel'; import { BroadcastChannel } from 'broadcast-channel';
import { defu } from 'defu';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { get, set } from '@/scripts/idb-proxy.js'; import { get, set } from '@/scripts/idb-proxy.js';
@ -80,6 +81,18 @@ export class Storage<T extends StateDef> {
this.loaded = this.ready.then(() => this.load()); this.loaded = this.ready.then(() => this.load());
} }
private isPureObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
private mergeState<T>(value: T, def: T): T {
if (this.isPureObject(value) && this.isPureObject(def)) {
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
return defu(value, def) as T;
}
return value;
}
private async init(): Promise<void> { private async init(): Promise<void> {
await this.migrate(); await this.migrate();
@ -89,11 +102,11 @@ export class Storage<T extends StateDef> {
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
this.reactiveState[k].value = this.state[k] = deviceState[k]; this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
this.reactiveState[k].value = this.state[k] = registryCache[k]; this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
this.reactiveState[k].value = this.state[k] = deviceAccountState[k]; this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else { } else {
this.reactiveState[k].value = this.state[k] = v.default; this.reactiveState[k].value = this.state[k] = v.default;
if (_DEV_) console.log('Use default value', k, v.default); if (_DEV_) console.log('Use default value', k, v.default);

View file

@ -24,11 +24,9 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@types/matter-js": "0.19.6",
"@types/node": "20.11.5", "@types/node": "20.11.5",
"@types/seedrandom": "3.0.8", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/parser": "6.18.1",
"@typescript-eslint/parser": "6.19.0",
"eslint": "8.56.0", "eslint": "8.56.0",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"typescript": "5.3.3" "typescript": "5.3.3"
@ -37,6 +35,8 @@
"built" "built"
], ],
"dependencies": { "dependencies": {
"@types/matter-js": "0.19.6",
"@types/seedrandom": "3.0.8",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"seedrandom": "3.0.5" "seedrandom": "3.0.5"

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021-2022 syuilo and other contributors Copyright (c) 2021-2024 syuilo and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -81,7 +81,17 @@ module.exports = {
// ], // ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {}, moduleNameMapper: {
// Do not resolve .wasm.js to .wasm by the rule below
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
// converts it again to `../../src/foo/bar` which then can be resolved to
// `.ts` files.
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
// directly import `.ts` files without this hack.
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [], // modulePathIgnorePatterns: [],

View file

@ -1,8 +1,9 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "0.0.16", "version": "2024.2.0-beta.3",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./built/esm/index.js", "import": "./built/esm/index.js",
@ -39,8 +40,8 @@
"@swc/jest": "0.2.31", "@swc/jest": "0.2.31",
"@types/jest": "29.5.11", "@types/jest": "29.5.11",
"@types/node": "20.11.5", "@types/node": "20.11.5",
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.19.0", "@typescript-eslint/parser": "6.18.1",
"eslint": "8.56.0", "eslint": "8.56.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-fetch-mock": "3.0.3", "jest-fetch-mock": "3.0.3",
@ -52,7 +53,9 @@
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"files": [ "files": [
"built" "built",
"built/esm",
"built/dts"
], ],
"dependencies": { "dependencies": {
"@swc/cli": "0.1.63", "@swc/cli": "0.1.63",

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.332Z * generatedAt: 2024-01-22T07:11:08.412Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.330Z * generatedAt: 2024-01-22T07:11:08.410Z
*/ */
import type { import type {

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.328Z * generatedAt: 2024-01-22T07:11:08.408Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.327Z * generatedAt: 2024-01-22T07:11:08.408Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -2,8 +2,8 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.246Z * generatedAt: 2024-01-22T07:11:08.327Z
*/ */
/** /**
@ -4602,10 +4602,6 @@ export type components = {
endedAt: string | null; endedAt: string | null;
isStarted: boolean; isStarted: boolean;
isEnded: boolean; isEnded: boolean;
form1: Record<string, never> | null;
form2: Record<string, never> | null;
user1Ready: boolean;
user2Ready: boolean;
/** Format: id */ /** Format: id */
user1Id: string; user1Id: string;
/** Format: id */ /** Format: id */

View file

@ -25,8 +25,8 @@
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@types/node": "20.11.5", "@types/node": "20.11.5",
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.19.0", "@typescript-eslint/parser": "6.18.1",
"eslint": "8.56.0", "eslint": "8.56.0",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"typescript": "5.3.3" "typescript": "5.3.3"

View file

@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@typescript-eslint/parser": "6.19.0", "@typescript-eslint/parser": "6.18.1",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",

File diff suppressed because it is too large Load diff