@lf_maple/koishi-plugin-phi-plugin-full 0.1.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (629) hide show
  1. package/LICENSE +22 -0
  2. package/core/.github/ISSUE_TEMPLATE/bug.yaml +133 -0
  3. package/core/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. package/core/.github/ISSUE_TEMPLATE/enhancement.yml +44 -0
  5. package/core/.vscode/c_cpp_properties.json +18 -0
  6. package/core/.vscode/launch.json +24 -0
  7. package/core/.vscode/settings.json +59 -0
  8. package/core/CHANGELOG.md +184 -0
  9. package/core/LICENSE +674 -0
  10. package/core/README.md +330 -0
  11. package/core/README_en.md +285 -0
  12. package/core/apps/Dan.js +129 -0
  13. package/core/apps/RankList.js +322 -0
  14. package/core/apps/aliasProposal.js +223 -0
  15. package/core/apps/apiSetting.js +592 -0
  16. package/core/apps/b19.js +1329 -0
  17. package/core/apps/botClient.js +64 -0
  18. package/core/apps/chart.js +692 -0
  19. package/core/apps/guessGame/guessIll.js +821 -0
  20. package/core/apps/guessGame/guessLetter.js +831 -0
  21. package/core/apps/guessGame/guessTips.js +254 -0
  22. package/core/apps/guessGame/letterGameUtils.js +74 -0
  23. package/core/apps/guessGame.js +214 -0
  24. package/core/apps/help.js +117 -0
  25. package/core/apps/manage.js +288 -0
  26. package/core/apps/market.js +243 -0
  27. package/core/apps/money.js +1064 -0
  28. package/core/apps/phisong.js +1418 -0
  29. package/core/apps/session.js +664 -0
  30. package/core/apps/setting.js +468 -0
  31. package/core/apps/update.js +541 -0
  32. package/core/apps/user.js +968 -0
  33. package/core/components/Config.js +249 -0
  34. package/core/components/Data.js +304 -0
  35. package/core/components/FileWatcherRegistry.js +102 -0
  36. package/core/components/Logger.js +11 -0
  37. package/core/components/ProcessCleanup.js +113 -0
  38. package/core/components/Version.js +56 -0
  39. package/core/components/YamlReader.js +157 -0
  40. package/core/components/baseClass.js +287 -0
  41. package/core/components/common.js +47 -0
  42. package/core/components/index.js +13 -0
  43. package/core/components/platform/index.js +79 -0
  44. package/core/components/platform/koishi.js +800 -0
  45. package/core/components/platform/koishiDatabaseRedis.js +391 -0
  46. package/core/components/platform/memoryRedis.js +209 -0
  47. package/core/components/platform/types.d.ts +272 -0
  48. package/core/components/platform/yunzai.js +604 -0
  49. package/core/components/segment.js +4 -0
  50. package/core/config/config//350/277/231/351/207/214/346/230/257/351/205/215/347/275/256/346/226/207/344/273/266/347/233/256/345/275/225.txt +0 -0
  51. package/core/config/default_config/banGroup.yaml +14 -0
  52. package/core/config/default_config/config.yaml +159 -0
  53. package/core/config/default_config/nickconfig.yaml +6 -0
  54. package/core/config/default_config/otherinfo.yaml +51 -0
  55. package/core/guoba.support.js +597 -0
  56. package/core/index.js +85 -0
  57. package/core/lib/AES.js +40 -0
  58. package/core/lib/Base64.js +22 -0
  59. package/core/lib/ByteReader.js +168 -0
  60. package/core/lib/GameProgress.js +37 -0
  61. package/core/lib/GameRecord.js +47 -0
  62. package/core/lib/GameSettings.js +24 -0
  63. package/core/lib/GameUser.js +19 -0
  64. package/core/lib/Level.js +6 -0
  65. package/core/lib/LevelRecord.js +9 -0
  66. package/core/lib/PhigrosUser.js +135 -0
  67. package/core/lib/SaveManager.js +117 -0
  68. package/core/lib/Summary.js +47 -0
  69. package/core/lib/TapTap/CompleteQRCodeData.js +27 -0
  70. package/core/lib/TapTap/LCHelper.js +81 -0
  71. package/core/lib/TapTap/TapTapHelper.js +170 -0
  72. package/core/lib/TapTap/getQRcode.js +61 -0
  73. package/core/lib/Util.js +30 -0
  74. package/core/lib/getQRcode.js +73 -0
  75. package/core/lib/http.js +105 -0
  76. package/core/model/README.md +3 -0
  77. package/core/model/api/aliasProposalService.js +151 -0
  78. package/core/model/api/apiVersion.js +67 -0
  79. package/core/model/api/autoSeekApi.js +165 -0
  80. package/core/model/api/botApiAuth.js +208 -0
  81. package/core/model/api/botSyncService.js +78 -0
  82. package/core/model/api/gameRecordPayload.js +24 -0
  83. package/core/model/api/makeRequest.js +1004 -0
  84. package/core/model/api/makeRequestFnc.js +17 -0
  85. package/core/model/api/phiApiClient.js +53 -0
  86. package/core/model/api/phiApiErrors.js +121 -0
  87. package/core/model/filesystem/getFile.js +256 -0
  88. package/core/model/filesystem/path.js +84 -0
  89. package/core/model/game/Chart.js +57 -0
  90. package/core/model/game/LevelRecordInfo.js +109 -0
  91. package/core/model/game/SongsInfo.js +46 -0
  92. package/core/model/game/b30Analysis.js +93 -0
  93. package/core/model/game/chartParser/Chart.js +23 -0
  94. package/core/model/game/chartParser/JudgeLine.js +38 -0
  95. package/core/model/game/chartParser/base/JudgeLineEvent.js +99 -0
  96. package/core/model/game/chartParser/base/Note.js +44 -0
  97. package/core/model/game/chartParser/base/SpeedEvent.js +25 -0
  98. package/core/model/game/chartParser/base/baseType.js +2 -0
  99. package/core/model/game/constNum.js +215 -0
  100. package/core/model/game/fCompute.js +711 -0
  101. package/core/model/game/getChartTag.js +140 -0
  102. package/core/model/game/getComment.js +132 -0
  103. package/core/model/game/getInfo.js +1069 -0
  104. package/core/model/game/getRksRank.js +77 -0
  105. package/core/model/game/markdown.js +77 -0
  106. package/core/model/integrations/Vika.js +103 -0
  107. package/core/model/integrations/getInfoFromTap.js +80 -0
  108. package/core/model/render/getPic.js +79 -0
  109. package/core/model/render/picmodle.js +496 -0
  110. package/core/model/render/progress-bar.js +44 -0
  111. package/core/model/render/puppeteer.js +431 -0
  112. package/core/model/render/renderPressureHistory.js +71 -0
  113. package/core/model/render/send.js +94 -0
  114. package/core/model/save/Save.js +953 -0
  115. package/core/model/save/analyzeSaveHistory.js +413 -0
  116. package/core/model/save/getBackup.js +328 -0
  117. package/core/model/save/getSave.js +90 -0
  118. package/core/model/save/getSaveFromApi.js +46 -0
  119. package/core/model/save/saveHistory.js +586 -0
  120. package/core/model/save/scoreHistory.js +101 -0
  121. package/core/model/theme/catalog.js +192 -0
  122. package/core/model/theme/installGuard.js +275 -0
  123. package/core/model/theme/installLock.js +174 -0
  124. package/core/model/theme/installer.js +398 -0
  125. package/core/model/theme/manager.js +408 -0
  126. package/core/model/theme/marketClient.js +294 -0
  127. package/core/model/theme/paths.js +42 -0
  128. package/core/model/theme/policy.js +54 -0
  129. package/core/model/theme/recovery.js +167 -0
  130. package/core/model/theme/useService.js +187 -0
  131. package/core/model/type/aliasProposal.d.ts +45 -0
  132. package/core/model/type/type.js +420 -0
  133. package/core/model/user/apiPermission.js +62 -0
  134. package/core/model/user/getBanGroup.js +120 -0
  135. package/core/model/user/getNotes.js +48 -0
  136. package/core/model/user/pluginData.js +101 -0
  137. package/core/model/user/userCredentialStore.js +154 -0
  138. package/core/model/user/userCredentials.js +860 -0
  139. package/core/package.json +60 -0
  140. package/core/resources/0608badSave/save.json +1 -0
  141. package/core/resources/LICENSE +201 -0
  142. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.art +220 -0
  143. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.css +345 -0
  144. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.html +434 -0
  145. package/core/resources/html/arcgrosB19/arcgros.png +0 -0
  146. package/core/resources/html/arcgrosB19/arcgrosB19-orz.html +276 -0
  147. package/core/resources/html/arcgrosB19/arcgrosB19.art +168 -0
  148. package/core/resources/html/arcgrosB19/arcgrosB19.css +637 -0
  149. package/core/resources/html/atlas/atlas-orz.html +313 -0
  150. package/core/resources/html/atlas/atlas.art +194 -0
  151. package/core/resources/html/atlas/atlas.css +695 -0
  152. package/core/resources/html/avatar/-SURREALISM-.png +0 -0
  153. package/core/resources/html/avatar/Alkaid.png +0 -0
  154. package/core/resources/html/avatar/Aphasia.png +0 -0
  155. package/core/resources/html/avatar/Artificial Existence.png +0 -0
  156. package/core/resources/html/avatar/Bonus Time.png +0 -0
  157. package/core/resources/html/avatar/Burn.png +0 -0
  158. package/core/resources/html/avatar/Cereris1.png +0 -0
  159. package/core/resources/html/avatar/Cereris2.png +0 -0
  160. package/core/resources/html/avatar/Cereris3.png +0 -0
  161. package/core/resources/html/avatar/Christmas -2019.png +0 -0
  162. package/core/resources/html/avatar/Cipher1.png +0 -0
  163. package/core/resources/html/avatar/Class Memories.png +0 -0
  164. package/core/resources/html/avatar/Credits.png +0 -0
  165. package/core/resources/html/avatar/Dash.png +0 -0
  166. package/core/resources/html/avatar/Dlyrotz.png +0 -0
  167. package/core/resources/html/avatar/Doppelganger1.png +0 -0
  168. package/core/resources/html/avatar/Doppelganger2.png +0 -0
  169. package/core/resources/html/avatar/Drop it.png +0 -0
  170. package/core/resources/html/avatar/Energy Synergy Matrix1.png +0 -0
  171. package/core/resources/html/avatar/Energy Synergy Matrix2.png +0 -0
  172. package/core/resources/html/avatar/Energy Synergy Matrix3.png +0 -0
  173. package/core/resources/html/avatar/Engine x Start!! (melody mix).png +0 -0
  174. package/core/resources/html/avatar/Engine x Start!! extra 1 -by cycats.png +0 -0
  175. package/core/resources/html/avatar/Eradication Catastrophe.png +0 -0
  176. package/core/resources/html/avatar/FULi AUTO SHOOTER extra 1 -by cycats.png +0 -0
  177. package/core/resources/html/avatar/FULi AUTO SHOOTER.png +0 -0
  178. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/264-Special1.png +0 -0
  179. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/264-Special2.png +0 -0
  180. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2641.png +0 -0
  181. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2642.png +0 -0
  182. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2643.png +0 -0
  183. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2644.png +0 -0
  184. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2645.png +0 -0
  185. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2646.png +0 -0
  186. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2647.png +0 -0
  187. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2648.png +0 -0
  188. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2649.png +0 -0
  189. package/core/resources/html/avatar/Find_Me.png +0 -0
  190. package/core/resources/html/avatar/Geopelia 1 -by cycats.png +0 -0
  191. package/core/resources/html/avatar/Geopelia 2 -by cycats.png +0 -0
  192. package/core/resources/html/avatar/Geopelia 3 -by /344/275/225/345/221/265.png +0 -0
  193. package/core/resources/html/avatar/Geopelia in WEPLAY -by /346/265/267/344/272/247.png +0 -0
  194. package/core/resources/html/avatar/Gino-AprilFool.png +0 -0
  195. package/core/resources/html/avatar/Glaciaxion.png +0 -0
  196. package/core/resources/html/avatar/HumaN.png +0 -0
  197. package/core/resources/html/avatar/Indelible Scar.png +0 -0
  198. package/core/resources/html/avatar/Introduction.png +0 -0
  199. package/core/resources/html/avatar/Khronostasis Katharsis.png +0 -0
  200. package/core/resources/html/avatar/MARENOL.png +0 -0
  201. package/core/resources/html/avatar/Marenol extra 1 -by /345/226/265n/350/221/265.png +0 -0
  202. package/core/resources/html/avatar/Non-Melodic Ragez (MUG Edit).png +0 -0
  203. package/core/resources/html/avatar/Non-Melodic Ragez extra 1 -by cycats.png +0 -0
  204. package/core/resources/html/avatar/Oblivion +0 -0
  205. package/core/resources/html/avatar/OblivionPHIN.png +0 -0
  206. package/core/resources/html/avatar/Para.png +0 -0
  207. package/core/resources/html/avatar/Pixel Rebelz1.png +0 -0
  208. package/core/resources/html/avatar/Pixel Rebelz2.png +0 -0
  209. package/core/resources/html/avatar/Pixel Rebelz3.png +0 -0
  210. package/core/resources/html/avatar/RIPPER extra 1 -by cycats.png +0 -0
  211. package/core/resources/html/avatar/RIPPER extra 2 -by /344/275/225/345/221/265.png +0 -0
  212. package/core/resources/html/avatar/Sein.png +0 -0
  213. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-Special.png +0 -0
  214. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-Special2.png +0 -0
  215. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-/346/231/256/351/200/232.png +0 -0
  216. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-/346/233/262/347/273/230Ver.png +0 -0
  217. package/core/resources/html/avatar/ShineAfter-/351/270/240-Special.png +0 -0
  218. package/core/resources/html/avatar/ShineAfter-/351/270/240-/346/231/256/351/200/232.png +0 -0
  219. package/core/resources/html/avatar/ShineAfter-/351/270/240-/346/233/262/347/273/230Ver.png +0 -0
  220. package/core/resources/html/avatar/ShineAfter-/351/270/240/345/222/214/346/260/264/351/235/222-Special.png +0 -0
  221. package/core/resources/html/avatar/Snow Desert.png +0 -0
  222. package/core/resources/html/avatar/Sparkle New Life.png +0 -0
  223. package/core/resources/html/avatar/Sultan Rage.png +0 -0
  224. package/core/resources/html/avatar/Winter /342/206/221cube/342/206/223.png +0 -0
  225. package/core/resources/html/avatar/Xenolith.png +0 -0
  226. package/core/resources/html/avatar/[PRAW].png +0 -0
  227. package/core/resources/html/avatar/dB doll.png +0 -0
  228. package/core/resources/html/avatar/ripper.png +0 -0
  229. package/core/resources/html/avatar//343/202/202/343/201/272/343/202/202/343/201/2721.png +0 -0
  230. package/core/resources/html/avatar//343/202/202/343/201/272/343/202/202/343/201/2722.png +0 -0
  231. package/core/resources/html/avatar//344/270/207/345/220/250/345/214/277/345/220/215/344/277/241.png +0 -0
  232. package/core/resources/html/avatar//344/272/221/345/245/263/345/255/251 extra 1 -by /345/276/241/345/235/202/346/236/234/345/255/220.png +0 -0
  233. package/core/resources/html/avatar//344/272/221/345/245/263/345/255/251.png +0 -0
  234. package/core/resources/html/avatar//345/205/211.png +0 -0
  235. package/core/resources/html/avatar//345/274/200/345/277/203/347/227/2051.png +0 -0
  236. package/core/resources/html/avatar//345/274/200/345/277/203/347/227/2052.png +0 -0
  237. package/core/resources/html/avatar//346/260/264/351/235/222-/344/270/200/345/221/250/345/271/264/347/233/270/350/201/232.png +0 -0
  238. package/core/resources/html/avatar//346/267/267/344/271/261-Confusion.png +0 -0
  239. package/core/resources/html/avatar//347/213/202/345/226/234/350/230/255/350/210/2361.png +0 -0
  240. package/core/resources/html/avatar//347/213/202/345/226/234/350/230/255/350/210/2362.png +0 -0
  241. package/core/resources/html/avatar//347/223/267/345/262/201-Special.png +0 -0
  242. package/core/resources/html/avatar//347/223/267/345/262/2011.png +0 -0
  243. package/core/resources/html/avatar//347/223/267/345/262/2012.png +0 -0
  244. package/core/resources/html/avatar//347/223/267/345/262/2013.png +0 -0
  245. package/core/resources/html/avatar//347/223/267/345/262/2014.png +0 -0
  246. package/core/resources/html/avatar//347/223/267/345/262/2015.png +0 -0
  247. package/core/resources/html/avatar//347/223/267/345/262/2016.png +0 -0
  248. package/core/resources/html/avatar//347/223/267/345/262/2017.png +0 -0
  249. package/core/resources/html/avatar//347/233/217/350/214/227-Special.png +0 -0
  250. package/core/resources/html/avatar//347/233/217/350/214/2271.png +0 -0
  251. package/core/resources/html/avatar//347/233/217/350/214/2272.png +0 -0
  252. package/core/resources/html/avatar//347/233/217/350/214/2273.png +0 -0
  253. package/core/resources/html/avatar//350/217/257/347/201/257/347/210/2611.png +0 -0
  254. package/core/resources/html/avatar//350/217/257/347/201/257/347/210/2612.png +0 -0
  255. package/core/resources/html/avatar//350/220/244/347/201/253/350/231/253/343/201/256/346/200/2501.png +0 -0
  256. package/core/resources/html/avatar//350/220/244/347/201/253/350/231/253/343/201/256/346/200/2502.png +0 -0
  257. package/core/resources/html/avatar//351/235/222/350/212/275-Special.png +0 -0
  258. package/core/resources/html/avatar//351/235/222/350/212/275.png +0 -0
  259. package/core/resources/html/avatar//351/243/216/345/261/277.png +0 -0
  260. package/core/resources/html/avatar//351/270/240-AprilFool.png +0 -0
  261. package/core/resources/html/avatar//351/270/240-/344/270/200/345/221/250/345/271/264/347/233/270/350/201/232.png +0 -0
  262. package/core/resources/html/b19/b19.art +490 -0
  263. package/core/resources/html/b19/b19.css +1261 -0
  264. package/core/resources/html/b19/b19666.art +350 -0
  265. package/core/resources/html/b19/b19666.css +1409 -0
  266. package/core/resources/html/b19/b19666.html +481 -0
  267. package/core/resources/html/b19/b19title.css +120 -0
  268. package/core/resources/html/b19/best19.html +853 -0
  269. package/core/resources/html/b19/dss2-orz.html +428 -0
  270. package/core/resources/html/b19/dss2.art +298 -0
  271. package/core/resources/html/b19/dss2.css +776 -0
  272. package/core/resources/html/b19/res/666.jpeg +0 -0
  273. package/core/resources/html/chap/chap-orz.html +93 -0
  274. package/core/resources/html/chap/chap.art +87 -0
  275. package/core/resources/html/chap/chap.css +365 -0
  276. package/core/resources/html/chartImg/chartImg.art +169 -0
  277. package/core/resources/html/chartImg/chartImg.css +439 -0
  278. package/core/resources/html/chartImg/echarts-wordcloud.min.js +3 -0
  279. package/core/resources/html/chartImg/echarts.js +85796 -0
  280. package/core/resources/html/chartImg/img/Drag.png +0 -0
  281. package/core/resources/html/chartImg/img/DragHL.png +0 -0
  282. package/core/resources/html/chartImg/img/Flick.png +0 -0
  283. package/core/resources/html/chartImg/img/FlickHL.png +0 -0
  284. package/core/resources/html/chartImg/img/Hold.png +0 -0
  285. package/core/resources/html/chartImg/img/HoldHL.png +0 -0
  286. package/core/resources/html/chartImg/img/Hold_End.png +0 -0
  287. package/core/resources/html/chartImg/img/Hold_Head.png +0 -0
  288. package/core/resources/html/chartImg/img/Hold_HeadHL.png +0 -0
  289. package/core/resources/html/chartImg/img/Tap.png +0 -0
  290. package/core/resources/html/chartImg/img/TapHL.png +0 -0
  291. package/core/resources/html/chartInfo/chartInfo-orz.html +250 -0
  292. package/core/resources/html/chartInfo/chartInfo.art +222 -0
  293. package/core/resources/html/chartInfo/chartInfo.css +265 -0
  294. package/core/resources/html/chartInfo/echarts-wordcloud.min.js +3 -0
  295. package/core/resources/html/chartInfo/echarts.js +85796 -0
  296. package/core/resources/html/clg/clg-orz.html +218 -0
  297. package/core/resources/html/clg/clg.art +88 -0
  298. package/core/resources/html/clg/clg.css +230 -0
  299. package/core/resources/html/clg/demo.jpg +0 -0
  300. package/core/resources/html/common/common.css +319 -0
  301. package/core/resources/html/common/css/playerInfo.css +136 -0
  302. package/core/resources/html/common/font/Aldrich-Regular.ttf +0 -0
  303. package/core/resources/html/common/font/HIMALAYA.TTF +0 -0
  304. package/core/resources/html/common/font/NotoColorEmoji-Regular.ttf +0 -0
  305. package/core/resources/html/common/font/NotoSans-Regular.ttf +0 -0
  306. package/core/resources/html/common/font/NotoSansArabic.ttf +0 -0
  307. package/core/resources/html/common/font/NotoSansCanadianAboriginal.ttf +0 -0
  308. package/core/resources/html/common/font/NotoSansJP.ttf +0 -0
  309. package/core/resources/html/common/font/NotoSansKannada.ttf +0 -0
  310. package/core/resources/html/common/font/NotoSansMath-Regular.ttf +0 -0
  311. package/core/resources/html/common/font/NotoSansSymbols2.ttf +0 -0
  312. package/core/resources/html/common/font/phi.ttf +0 -0
  313. package/core/resources/html/common/font//345/220/236/345/274/245/346/201/260/344/277/212.ttf +0 -0
  314. package/core/resources/html/common/layout/default.art +324 -0
  315. package/core/resources/html/common/layout/elem.art +21 -0
  316. package/core/resources/html/common/theme/snow/snow.css +17 -0
  317. package/core/resources/html/common/theme/snow/snow.js +19 -0
  318. package/core/resources/html/common/theme/star/star.js +201 -0
  319. package/core/resources/html/common/theme/topText/topText.css +15 -0
  320. package/core/resources/html/common/theme/topText/topText.js +30 -0
  321. package/core/resources/html/difficultyHistory/difficultyHistory-orz.html +361 -0
  322. package/core/resources/html/difficultyHistory/difficultyHistory.art +102 -0
  323. package/core/resources/html/difficultyHistory/difficultyHistory.css +205 -0
  324. package/core/resources/html/guess/guess-orz.html +50 -0
  325. package/core/resources/html/guess/guess.art +51 -0
  326. package/core/resources/html/guess/guess.css +33 -0
  327. package/core/resources/html/help/help-orz.html +73 -0
  328. package/core/resources/html/help/help.art +84 -0
  329. package/core/resources/html/help/help.css +266 -0
  330. package/core/resources/html/historyB30/historyB30-orz.html +168 -0
  331. package/core/resources/html/historyB30/historyB30.art +143 -0
  332. package/core/resources/html/historyB30/historyB30.css +270 -0
  333. package/core/resources/html/ill/ill-orz.html +21 -0
  334. package/core/resources/html/ill/ill.art +23 -0
  335. package/core/resources/html/ill/ill.css +79 -0
  336. package/core/resources/html/jrrp/ShineAfter.removebg.png +0 -0
  337. package/core/resources/html/jrrp/jrrp-orz.html +68 -0
  338. package/core/resources/html/jrrp/jrrp.art +58 -0
  339. package/core/resources/html/jrrp/jrrp.css +251 -0
  340. package/core/resources/html/list/list-orz.html +35 -0
  341. package/core/resources/html/list/list.art +67 -0
  342. package/core/resources/html/list/list.css +259 -0
  343. package/core/resources/html/lvsco/demo.jpg +0 -0
  344. package/core/resources/html/lvsco/lvsco-orz.html +334 -0
  345. package/core/resources/html/lvsco/lvsco.art +314 -0
  346. package/core/resources/html/lvsco/lvsco.css +706 -0
  347. package/core/resources/html/market/detail.art +44 -0
  348. package/core/resources/html/market/market.art +86 -0
  349. package/core/resources/html/market/market.css +64 -0
  350. package/core/resources/html/newSong/newSong.art +34 -0
  351. package/core/resources/html/newSong/newSong.css +48 -0
  352. package/core/resources/html/newSong/newSong.html +23 -0
  353. package/core/resources/html/newnotice/newnotice.art +47 -0
  354. package/core/resources/html/newnotice/newnotice.css +83 -0
  355. package/core/resources/html/otherimg/0.png +0 -0
  356. package/core/resources/html/otherimg/1.png +0 -0
  357. package/core/resources/html/otherimg/2.png +0 -0
  358. package/core/resources/html/otherimg/3.png +0 -0
  359. package/core/resources/html/otherimg/4.png +0 -0
  360. package/core/resources/html/otherimg/5.png +0 -0
  361. package/core/resources/html/otherimg/A.png +0 -0
  362. package/core/resources/html/otherimg/B.png +0 -0
  363. package/core/resources/html/otherimg/C.png +0 -0
  364. package/core/resources/html/otherimg/F.png +0 -0
  365. package/core/resources/html/otherimg/FC.png +0 -0
  366. package/core/resources/html/otherimg/NEW.png +0 -0
  367. package/core/resources/html/otherimg/Phigros_Icon_3.0.0.png +0 -0
  368. package/core/resources/html/otherimg/S.png +0 -0
  369. package/core/resources/html/otherimg/Star1.png +0 -0
  370. package/core/resources/html/otherimg/Star2.png +0 -0
  371. package/core/resources/html/otherimg/V.png +0 -0
  372. package/core/resources/html/otherimg/callback.png +0 -0
  373. package/core/resources/html/otherimg/chapHelp.png +0 -0
  374. package/core/resources/html/otherimg/dan_code.png +0 -0
  375. package/core/resources/html/otherimg/data.png +0 -0
  376. package/core/resources/html/otherimg/helpDoc.png +0 -0
  377. package/core/resources/html/otherimg/nickSuggest.png +0 -0
  378. package/core/resources/html/otherimg/phi.png +0 -0
  379. package/core/resources/html/otherimg/phigros.png +0 -0
  380. package/core/resources/html/otherimg/snow1.png +0 -0
  381. package/core/resources/html/otherimg/snow2.png +0 -0
  382. package/core/resources/html/otherimg/snow3.png +0 -0
  383. package/core/resources/html/otherimg/snow4.png +0 -0
  384. package/core/resources/html/otherimg/title.png +0 -0
  385. package/core/resources/html/rand/rand-orz.html +62 -0
  386. package/core/resources/html/rand/rand.art +59 -0
  387. package/core/resources/html/rand/rand.css +257 -0
  388. package/core/resources/html/rankingList/rankingList-orz.html +456 -0
  389. package/core/resources/html/rankingList/rankingList.art +171 -0
  390. package/core/resources/html/rankingList/rankingList.css +570 -0
  391. package/core/resources/html/rankingList-old/rankingList-orz.html +300 -0
  392. package/core/resources/html/rankingList-old/rankingList.art +89 -0
  393. package/core/resources/html/rankingList-old/rankingList.css +265 -0
  394. package/core/resources/html/score/score-orz.html +625 -0
  395. package/core/resources/html/score/score.art +249 -0
  396. package/core/resources/html/score/score.css +435 -0
  397. package/core/resources/html/score/scoreOld-orz.html +128 -0
  398. package/core/resources/html/score/scoreOld.art +230 -0
  399. package/core/resources/html/score/scoreOld.css +475 -0
  400. package/core/resources/html/score/scoreRankList-orz.html +613 -0
  401. package/core/resources/html/score/scoreRankList.art +231 -0
  402. package/core/resources/html/score/scoreRankList.css +197 -0
  403. package/core/resources/html/setting/setting-orz.html +123 -0
  404. package/core/resources/html/setting/setting.art +72 -0
  405. package/core/resources/html/setting/setting.css +170 -0
  406. package/core/resources/html/setting/userSetting-orz.html +132 -0
  407. package/core/resources/html/setting/userSetting.art +48 -0
  408. package/core/resources/html/setting/userSetting.css +160 -0
  409. package/core/resources/html/sign/sign-orz.html +262 -0
  410. package/core/resources/html/sign/sign.art +163 -0
  411. package/core/resources/html/sign/sign.css +706 -0
  412. package/core/resources/html/suggest/suggest.art +271 -0
  413. package/core/resources/html/suggest/suggest.css +300 -0
  414. package/core/resources/html/table/table-orz.html +228 -0
  415. package/core/resources/html/table/table.art +163 -0
  416. package/core/resources/html/table/table.css +401 -0
  417. package/core/resources/html/tasks/tasks-orz.html +83 -0
  418. package/core/resources/html/tasks/tasks.art +118 -0
  419. package/core/resources/html/tasks/tasks.css +334 -0
  420. package/core/resources/html/update/update-orz.html +735 -0
  421. package/core/resources/html/update/update.art +259 -0
  422. package/core/resources/html/update/update.css +676 -0
  423. package/core/resources/html/userinfo/userinfo-old-orz.html +408 -0
  424. package/core/resources/html/userinfo/userinfo-old.art +233 -0
  425. package/core/resources/html/userinfo/userinfo-old.css +466 -0
  426. package/core/resources/html/userinfo/userinfo-orz.html +543 -0
  427. package/core/resources/html/userinfo/userinfo.art +288 -0
  428. package/core/resources/html/userinfo/userinfo.css +757 -0
  429. package/core/resources/info/DLC/arc.json +412 -0
  430. package/core/resources/info/DLC/chunithm.json +1172 -0
  431. package/core/resources/info/DLC/dynamix.json +455 -0
  432. package/core/resources/info/DLC/fanshi.json +132 -0
  433. package/core/resources/info/DLC/lanota.json +412 -0
  434. package/core/resources/info/DLC/maimai.json +1110 -0
  435. package/core/resources/info/DLC/orz.json +158 -0
  436. package/core/resources/info/DLC/rizline.json +40 -0
  437. package/core/resources/info/DLC/rotaeno.json +159 -0
  438. package/core/resources/info/DLC/ryceam.json +53 -0
  439. package/core/resources/info/avatar.txt +109 -0
  440. package/core/resources/info/chaplist.yaml +102 -0
  441. package/core/resources/info/help/api.json +83 -0
  442. package/core/resources/info/help.json +436 -0
  443. package/core/resources/info/info.csv +315 -0
  444. package/core/resources/info/infolist.json +1758 -0
  445. package/core/resources/info/jrrp.json +92 -0
  446. package/core/resources/info/nicklist.yaml +2373 -0
  447. package/core/resources/info/notesInfo.json +1 -0
  448. package/core/resources/info/notice.json +11 -0
  449. package/core/resources/info/oldInfo/100/change.csv +218 -0
  450. package/core/resources/info/oldInfo/100/info.json +7 -0
  451. package/core/resources/info/oldInfo/101/change.csv +218 -0
  452. package/core/resources/info/oldInfo/101/info.json +7 -0
  453. package/core/resources/info/oldInfo/102/change.csv +223 -0
  454. package/core/resources/info/oldInfo/102/info.json +7 -0
  455. package/core/resources/info/oldInfo/103/change.csv +224 -0
  456. package/core/resources/info/oldInfo/103/info.json +7 -0
  457. package/core/resources/info/oldInfo/104/change.csv +225 -0
  458. package/core/resources/info/oldInfo/104/info.json +7 -0
  459. package/core/resources/info/oldInfo/105/change.csv +228 -0
  460. package/core/resources/info/oldInfo/105/info.json +7 -0
  461. package/core/resources/info/oldInfo/106/change.csv +229 -0
  462. package/core/resources/info/oldInfo/106/info.json +7 -0
  463. package/core/resources/info/oldInfo/108/change.csv +233 -0
  464. package/core/resources/info/oldInfo/108/info.json +7 -0
  465. package/core/resources/info/oldInfo/109/change.csv +235 -0
  466. package/core/resources/info/oldInfo/109/info.json +7 -0
  467. package/core/resources/info/oldInfo/110/change.csv +235 -0
  468. package/core/resources/info/oldInfo/110/info.json +7 -0
  469. package/core/resources/info/oldInfo/111/change.csv +242 -0
  470. package/core/resources/info/oldInfo/111/info.json +7 -0
  471. package/core/resources/info/oldInfo/112/change.csv +242 -0
  472. package/core/resources/info/oldInfo/112/info.json +7 -0
  473. package/core/resources/info/oldInfo/113/change.csv +244 -0
  474. package/core/resources/info/oldInfo/113/info.json +7 -0
  475. package/core/resources/info/oldInfo/115/change.csv +247 -0
  476. package/core/resources/info/oldInfo/115/info.json +7 -0
  477. package/core/resources/info/oldInfo/116/change.csv +249 -0
  478. package/core/resources/info/oldInfo/116/info.json +7 -0
  479. package/core/resources/info/oldInfo/118/change.csv +252 -0
  480. package/core/resources/info/oldInfo/118/info.json +7 -0
  481. package/core/resources/info/oldInfo/119/change.csv +255 -0
  482. package/core/resources/info/oldInfo/119/info.json +7 -0
  483. package/core/resources/info/oldInfo/120/change.csv +255 -0
  484. package/core/resources/info/oldInfo/120/info.json +7 -0
  485. package/core/resources/info/oldInfo/121/change.csv +257 -0
  486. package/core/resources/info/oldInfo/121/info.json +7 -0
  487. package/core/resources/info/oldInfo/122/change.csv +260 -0
  488. package/core/resources/info/oldInfo/122/info.json +7 -0
  489. package/core/resources/info/oldInfo/123/change.csv +260 -0
  490. package/core/resources/info/oldInfo/123/info.json +7 -0
  491. package/core/resources/info/oldInfo/124/change.csv +262 -0
  492. package/core/resources/info/oldInfo/124/info.json +7 -0
  493. package/core/resources/info/oldInfo/125/change.csv +266 -0
  494. package/core/resources/info/oldInfo/125/info.json +7 -0
  495. package/core/resources/info/oldInfo/126/change.csv +269 -0
  496. package/core/resources/info/oldInfo/126/info.json +7 -0
  497. package/core/resources/info/oldInfo/127/change.csv +270 -0
  498. package/core/resources/info/oldInfo/127/info.json +7 -0
  499. package/core/resources/info/oldInfo/128/change.csv +273 -0
  500. package/core/resources/info/oldInfo/128/info.json +7 -0
  501. package/core/resources/info/oldInfo/129/change.csv +274 -0
  502. package/core/resources/info/oldInfo/129/info.json +7 -0
  503. package/core/resources/info/oldInfo/130/change.csv +275 -0
  504. package/core/resources/info/oldInfo/130/info.json +7 -0
  505. package/core/resources/info/oldInfo/131/change.csv +278 -0
  506. package/core/resources/info/oldInfo/131/info.json +7 -0
  507. package/core/resources/info/oldInfo/132/change.csv +280 -0
  508. package/core/resources/info/oldInfo/132/info.json +7 -0
  509. package/core/resources/info/oldInfo/133/change.csv +284 -0
  510. package/core/resources/info/oldInfo/133/info.json +7 -0
  511. package/core/resources/info/oldInfo/134/change.csv +286 -0
  512. package/core/resources/info/oldInfo/134/info.json +7 -0
  513. package/core/resources/info/oldInfo/135/change.csv +290 -0
  514. package/core/resources/info/oldInfo/135/info.json +7 -0
  515. package/core/resources/info/oldInfo/136/change.csv +290 -0
  516. package/core/resources/info/oldInfo/136/info.json +7 -0
  517. package/core/resources/info/oldInfo/137/change.csv +293 -0
  518. package/core/resources/info/oldInfo/137/info.json +7 -0
  519. package/core/resources/info/oldInfo/138/change.csv +295 -0
  520. package/core/resources/info/oldInfo/138/info.json +7 -0
  521. package/core/resources/info/oldInfo/139/change.csv +295 -0
  522. package/core/resources/info/oldInfo/139/info.json +7 -0
  523. package/core/resources/info/oldInfo/140/change.csv +298 -0
  524. package/core/resources/info/oldInfo/140/info.json +7 -0
  525. package/core/resources/info/oldInfo/141/change.csv +300 -0
  526. package/core/resources/info/oldInfo/141/info.json +7 -0
  527. package/core/resources/info/oldInfo/142/change.csv +302 -0
  528. package/core/resources/info/oldInfo/142/info.json +7 -0
  529. package/core/resources/info/oldInfo/143/change.csv +303 -0
  530. package/core/resources/info/oldInfo/143/info.json +7 -0
  531. package/core/resources/info/oldInfo/144/change.csv +303 -0
  532. package/core/resources/info/oldInfo/144/info.json +7 -0
  533. package/core/resources/info/oldInfo/145/change.csv +305 -0
  534. package/core/resources/info/oldInfo/145/info.json +7 -0
  535. package/core/resources/info/oldInfo/146/change.csv +305 -0
  536. package/core/resources/info/oldInfo/146/info.json +7 -0
  537. package/core/resources/info/oldInfo/148/change.csv +307 -0
  538. package/core/resources/info/oldInfo/148/info.json +7 -0
  539. package/core/resources/info/oldInfo/149/change.csv +307 -0
  540. package/core/resources/info/oldInfo/149/info.json +7 -0
  541. package/core/resources/info/oldInfo/151/change.csv +309 -0
  542. package/core/resources/info/oldInfo/151/info.json +7 -0
  543. package/core/resources/info/oldInfo/152/change.csv +312 -0
  544. package/core/resources/info/oldInfo/152/info.json +7 -0
  545. package/core/resources/info/oldInfo/153/change.csv +313 -0
  546. package/core/resources/info/oldInfo/153/info.json +7 -0
  547. package/core/resources/info/oldInfo/154/change.csv +315 -0
  548. package/core/resources/info/oldInfo/154/info.json +7 -0
  549. package/core/resources/info/oldInfo/92/change.csv +204 -0
  550. package/core/resources/info/oldInfo/92/info.json +7 -0
  551. package/core/resources/info/oldInfo/93/change.csv +206 -0
  552. package/core/resources/info/oldInfo/93/info.json +7 -0
  553. package/core/resources/info/oldInfo/94/change.csv +209 -0
  554. package/core/resources/info/oldInfo/94/info.json +7 -0
  555. package/core/resources/info/oldInfo/95/change.csv +211 -0
  556. package/core/resources/info/oldInfo/95/info.json +7 -0
  557. package/core/resources/info/oldInfo/96/change.csv +216 -0
  558. package/core/resources/info/oldInfo/96/info.json +7 -0
  559. package/core/resources/info/oldInfo/98/change.csv +216 -0
  560. package/core/resources/info/oldInfo/98/info.json +7 -0
  561. package/core/resources/info/oldInfo/99/change.csv +217 -0
  562. package/core/resources/info/oldInfo/99/info.json +7 -0
  563. package/core/resources/info/oldNotesInfo.json +1 -0
  564. package/core/resources/info/sentences.json +1 -0
  565. package/core/resources/info/spinfo.json +362 -0
  566. package/core/resources/info/tips.txt +278 -0
  567. package/core/resources/otherill//351/233/252/351/231/215SP.png +0 -0
  568. package/core/resources/song-select.html +535 -0
  569. package/core/resources/themes/README.md +24 -0
  570. package/core/resources/themes/milthm/A.png +0 -0
  571. package/core/resources/themes/milthm/B.png +0 -0
  572. package/core/resources/themes/milthm/C.png +0 -0
  573. package/core/resources/themes/milthm/F.png +0 -0
  574. package/core/resources/themes/milthm/FC.png +0 -0
  575. package/core/resources/themes/milthm/S.png +0 -0
  576. package/core/resources/themes/milthm/V.png +0 -0
  577. package/core/resources/themes/milthm/arcgrosB19.css +36 -0
  578. package/core/resources/themes/milthm/b19.art +464 -0
  579. package/core/resources/themes/milthm/b19.css +1314 -0
  580. package/core/resources/themes/milthm/bg.png +0 -0
  581. package/core/resources/themes/milthm/clg.css +37 -0
  582. package/core/resources/themes/milthm/difficultyHistory.css +42 -0
  583. package/core/resources/themes/milthm/font.ttf +0 -0
  584. package/core/resources/themes/milthm/help.css +36 -0
  585. package/core/resources/themes/milthm/historyB30.css +48 -0
  586. package/core/resources/themes/milthm/info.yaml +45 -0
  587. package/core/resources/themes/milthm/list.css +39 -0
  588. package/core/resources/themes/milthm/phi.png +0 -0
  589. package/core/resources/themes/milthm/setting.css +37 -0
  590. package/core/resources/themes/milthm/sign.css +81 -0
  591. package/core/resources/themes/milthm/suggest.css +40 -0
  592. package/core/resources/themes/milthm/table.css +54 -0
  593. package/core/resources/themes/milthm/update.css +68 -0
  594. package/core/tests/aliasProposalService.test.js +82 -0
  595. package/core/tests/apiAccountDeletion.test.js +125 -0
  596. package/core/tests/apiPermission.test.js +44 -0
  597. package/core/tests/apiVersion.test.js +55 -0
  598. package/core/tests/b30Analysis.test.js +38 -0
  599. package/core/tests/backgroundIllustration.test.js +21 -0
  600. package/core/tests/botApiAuthRecovery.test.js +148 -0
  601. package/core/tests/botSyncService.test.js +42 -0
  602. package/core/tests/fileWatcherRegistry.test.js +44 -0
  603. package/core/tests/gameRecordPayload.test.js +34 -0
  604. package/core/tests/guessLetter.test.js +79 -0
  605. package/core/tests/makeRequestHandling.test.js +118 -0
  606. package/core/tests/marketOverflow.test.js +31 -0
  607. package/core/tests/platformTemplateCache.test.js +92 -0
  608. package/core/tests/pluginDataThemePreference.test.js +36 -0
  609. package/core/tests/processCleanup.test.js +58 -0
  610. package/core/tests/puppeteerShutdown.test.js +17 -0
  611. package/core/tests/renderPressureHistory.test.js +59 -0
  612. package/core/tests/saveStats.test.js +26 -0
  613. package/core/tests/themeInstallGuard.test.js +289 -0
  614. package/core/tests/themeManager.test.js +625 -0
  615. package/core/tests/themeManagerInit.test.js +11 -0
  616. package/core/tests/themeMarket.test.js +855 -0
  617. package/core/tests/themePageRouting.test.js +431 -0
  618. package/core/tests/themePolicy.test.js +28 -0
  619. package/core/tests/themeStorageRecovery.test.js +192 -0
  620. package/core/tests/userCredentialModuleBoundaries.test.js +41 -0
  621. package/core/tests/userCredentials.test.js +285 -0
  622. package/core/tsconfig.json +32 -0
  623. package/core/types/vendor.d.ts +14 -0
  624. package/lib/index.cjs +358 -0
  625. package/lib/index.d.cts +182 -0
  626. package/lib/index.d.ts +182 -0
  627. package/lib/index.js +320 -0
  628. package/package.json +74 -0
  629. package/readme.md +3 -0
@@ -0,0 +1,294 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import Config from '../../components/Config.js'
4
+ import makeRequest from '../api/makeRequest.js'
5
+ import { classifyApiConnectionError } from '../api/phiApiErrors.js'
6
+
7
+ const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
8
+ const AUTH_ATTEMPTS = 3
9
+ const DOWNLOAD_ATTEMPTS = 2
10
+ const DOWNLOAD_TIMEOUT_MS = 60_000
11
+
12
+ export class ThemeMarketClientError extends Error {
13
+ /** @param {string} code @param {number} [status] */
14
+ constructor(code, status = 0) {
15
+ super(code)
16
+ this.name = 'ThemeMarketClientError'
17
+ this.code = code
18
+ this.status = status
19
+ }
20
+ }
21
+
22
+ const SLUG_RE = /^[a-z][a-z0-9_-]{0,119}$/
23
+
24
+ /** @param {unknown} value */
25
+ function isRecord(value) {
26
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
27
+ }
28
+
29
+ /**
30
+ * The API has shipped both response-level and theme-level capability fields.
31
+ * Accept either layout, but never guess when the field is absent or conflicts.
32
+ * @param {any} response
33
+ * @param {any} theme
34
+ */
35
+ export function resolveBotDownloadAllowed(response, theme) {
36
+ const hasResponseValue = isRecord(response) && Object.hasOwn(response, 'botDownloadAllowed')
37
+ const hasThemeValue = isRecord(theme) && Object.hasOwn(theme, 'botDownloadAllowed')
38
+ const responseValue = hasResponseValue ? response.botDownloadAllowed : undefined
39
+ const themeValue = hasThemeValue ? theme.botDownloadAllowed : undefined
40
+ if (
41
+ hasResponseValue && typeof responseValue !== 'boolean'
42
+ || hasThemeValue && typeof themeValue !== 'boolean'
43
+ || !hasResponseValue && !hasThemeValue
44
+ || hasResponseValue && hasThemeValue && responseValue !== themeValue
45
+ ) {
46
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
47
+ }
48
+ return /** @type {boolean} */ (hasThemeValue ? themeValue : responseValue)
49
+ }
50
+
51
+ /** @param {any} theme @param {boolean} botDownloadAllowed */
52
+ function validateTheme(theme, botDownloadAllowed) {
53
+ if (!isRecord(theme)
54
+ || typeof theme.slug !== 'string' || !SLUG_RE.test(theme.slug)
55
+ || typeof theme.name !== 'string' || !theme.name.trim() || theme.name.length > 100
56
+ || !['public', 'restricted', 'bot_only'].includes(theme.downloadPolicy)) {
57
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
58
+ }
59
+ return { ...theme, name: theme.name.trim(), botDownloadAllowed }
60
+ }
61
+
62
+ export async function getAvailableMarketThemes() {
63
+ const response = await makeRequest.getThemeMarketList()
64
+ if (!isRecord(response) || response.ok !== true || !Array.isArray(response.themes) || response.themes.length > 500) {
65
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
66
+ }
67
+ return response.themes.map((/** @type {any} */ theme) => validateTheme(
68
+ theme,
69
+ resolveBotDownloadAllowed(response, theme),
70
+ ))
71
+ }
72
+
73
+ /** @param {string} themeId */
74
+ export async function getAvailableMarketTheme(themeId) {
75
+ const response = await makeRequest.getThemeMarketDetail(themeId)
76
+ if (!isRecord(response) || response.ok !== true || !isRecord(response.theme)) {
77
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
78
+ }
79
+ const theme = validateTheme(response.theme, resolveBotDownloadAllowed(response, response.theme))
80
+ if (theme.slug !== themeId) throw new ThemeMarketClientError('theme_store_invalid_response', 502)
81
+ if (!theme.botDownloadAllowed) throw new ThemeMarketClientError('theme_not_allowed_by_bot', 403)
82
+ return theme
83
+ }
84
+
85
+ /** @param {number} ms */
86
+ function sleep(ms) {
87
+ return new Promise(resolve => setTimeout(resolve, ms))
88
+ }
89
+
90
+ /** @param {any} error @param {number} attempt */
91
+ function retryDelay(error, attempt) {
92
+ const raw = error?.data?.retryAfter
93
+ if (raw) {
94
+ const seconds = Number(raw)
95
+ const date = Date.parse(raw)
96
+ const value = Number.isFinite(seconds) ? seconds * 1000 : date - Date.now()
97
+ if (Number.isFinite(value)) return Math.max(0, Math.min(value, 15_000))
98
+ }
99
+ return 250 * (2 ** attempt) + Math.floor(Math.random() * 150)
100
+ }
101
+
102
+ /** @param {any} error */
103
+ function retryableAuthorization(error) {
104
+ const status = Number(error?.status || 0)
105
+ return status === 0 || [408, 425, 429].includes(status) || status >= 500
106
+ }
107
+
108
+ function configuredDownloadOrigin() {
109
+ const raw = String(Config.getUserCfg('config', 'themeMarketDownloadOrigin') || '').trim()
110
+ let url
111
+ try {
112
+ url = new URL(raw)
113
+ } catch {
114
+ throw new ThemeMarketClientError('theme_market_origin_invalid')
115
+ }
116
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash || url.search || url.pathname !== '/') {
117
+ throw new ThemeMarketClientError('theme_market_origin_invalid')
118
+ }
119
+ return url.origin
120
+ }
121
+
122
+ /** @param {any} value @param {string} themeId */
123
+ function validateDownload(value, themeId) {
124
+ const download = value?.download
125
+ let url
126
+ try {
127
+ url = new URL(download?.downloadUrl)
128
+ } catch {
129
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
130
+ }
131
+ if (
132
+ value?.ok !== true
133
+ || typeof download?.downloadId !== 'string'
134
+ || download.downloadId.length < 1
135
+ || download.downloadId.length > 128
136
+ || download.themeId !== themeId
137
+ || typeof download.version !== 'string'
138
+ || download.version.length < 1
139
+ || download.version.length > 64
140
+ || typeof download.fileName !== 'string'
141
+ || download.fileName.length < 1
142
+ || download.fileName.length > 255
143
+ || /[\\/\u0000-\u001f\u007f]/.test(download.fileName)
144
+ || download.contentType !== 'application/zip'
145
+ || !Number.isSafeInteger(download.size)
146
+ || download.size <= 0
147
+ || download.size > MAX_ARCHIVE_BYTES
148
+ || !/^[a-f0-9]{64}$/.test(download.sha256)
149
+ || !Number.isFinite(Date.parse(download.expiresAt))
150
+ || Date.parse(download.expiresAt) <= Date.now()
151
+ || url.protocol !== 'https:'
152
+ || url.origin !== configuredDownloadOrigin()
153
+ || url.username
154
+ || url.password
155
+ || url.hash
156
+ ) {
157
+ throw new ThemeMarketClientError('theme_store_invalid_response', 502)
158
+ }
159
+ return {
160
+ downloadId: download.downloadId,
161
+ themeId: download.themeId,
162
+ version: download.version,
163
+ fileName: download.fileName,
164
+ contentType: 'application/zip',
165
+ size: download.size,
166
+ sha256: download.sha256,
167
+ downloadUrl: url.toString(),
168
+ expiresAt: download.expiresAt,
169
+ }
170
+ }
171
+
172
+ /**
173
+ * @param {string} themeId
174
+ * @param {string} [requestId]
175
+ */
176
+ export async function authorizeThemeDownload(themeId, requestId = crypto.randomUUID()) {
177
+ for (let attempt = 0; attempt < AUTH_ATTEMPTS; attempt++) {
178
+ try {
179
+ const response = await makeRequest.requestThemeDownload({ requestId, themeId })
180
+ return validateDownload(response, themeId)
181
+ } catch (error) {
182
+ if (!retryableAuthorization(error) || attempt + 1 >= AUTH_ATTEMPTS) throw error
183
+ await sleep(retryDelay(error, attempt))
184
+ }
185
+ }
186
+ throw new ThemeMarketClientError('theme_store_unavailable', 503)
187
+ }
188
+
189
+ /** @param {fs.promises.FileHandle} handle @param {Uint8Array} chunk @param {number} position */
190
+ async function writeChunk(handle, chunk, position) {
191
+ let written = 0
192
+ while (written < chunk.byteLength) {
193
+ const result = await handle.write(chunk, written, chunk.byteLength - written, position + written)
194
+ if (result.bytesWritten <= 0) throw new ThemeMarketClientError('theme_download_write_failed')
195
+ written += result.bytesWritten
196
+ }
197
+ }
198
+
199
+ /**
200
+ * @param {ReturnType<typeof validateDownload>} download
201
+ * @param {string} targetPath
202
+ * @param {{fetchImpl?:typeof fetch}} [options]
203
+ */
204
+ export async function downloadThemeArchive(download, targetPath, options = {}) {
205
+ const fetchImpl = options.fetchImpl || fetch
206
+ let lastError = null
207
+ for (let attempt = 0; attempt < DOWNLOAD_ATTEMPTS; attempt++) {
208
+ if (Date.parse(download.expiresAt) <= Date.now()) {
209
+ throw new ThemeMarketClientError('theme_download_url_expired', 410)
210
+ }
211
+ await fs.promises.rm(targetPath, { force: true }).catch(() => {})
212
+ let handle
213
+ try {
214
+ const url = new URL(download.downloadUrl)
215
+ if (url.origin !== configuredDownloadOrigin() || url.protocol !== 'https:' || url.username || url.password || url.hash) {
216
+ throw new ThemeMarketClientError('theme_download_url_untrusted')
217
+ }
218
+ let response
219
+ try {
220
+ response = await fetchImpl(url, {
221
+ method: 'GET',
222
+ headers: { Accept: 'application/zip' },
223
+ redirect: 'error',
224
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
225
+ })
226
+ } catch (error) {
227
+ throw classifyApiConnectionError(error)
228
+ }
229
+ if ([401, 403, 410].includes(response.status)) {
230
+ await response.body?.cancel()
231
+ throw new ThemeMarketClientError('theme_download_url_expired', response.status)
232
+ }
233
+ if (!response.ok) {
234
+ await response.body?.cancel()
235
+ const error = new ThemeMarketClientError('theme_download_unavailable', response.status)
236
+ // @ts-ignore response headers are deliberately reduced to the one retry hint
237
+ error.retryAfter = response.headers.get('retry-after')
238
+ throw error
239
+ }
240
+ if (!response.body) throw new ThemeMarketClientError('theme_download_empty')
241
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0].trim()
242
+ const contentLength = response.headers.get('content-length')
243
+ if (contentType !== 'application/zip' || (contentLength !== null && contentLength !== String(download.size))) {
244
+ await response.body.cancel()
245
+ throw new ThemeMarketClientError('theme_package_integrity_failed')
246
+ }
247
+
248
+ handle = await fs.promises.open(targetPath, 'wx', 0o600)
249
+ const reader = response.body.getReader()
250
+ const hash = crypto.createHash('sha256')
251
+ let total = 0
252
+ while (true) {
253
+ let chunk
254
+ try {
255
+ chunk = await reader.read()
256
+ } catch (error) {
257
+ throw classifyApiConnectionError(error)
258
+ }
259
+ const { done, value } = chunk
260
+ if (done) break
261
+ if (!value) continue
262
+ total += value.byteLength
263
+ if (total > download.size || total > MAX_ARCHIVE_BYTES) {
264
+ await reader.cancel()
265
+ throw new ThemeMarketClientError('theme_package_integrity_failed')
266
+ }
267
+ await writeChunk(handle, value, total - value.byteLength)
268
+ hash.update(value)
269
+ }
270
+ await handle.sync()
271
+ await handle.close()
272
+ handle = undefined
273
+ if (total !== download.size || hash.digest('hex') !== download.sha256) {
274
+ throw new ThemeMarketClientError('theme_package_integrity_failed')
275
+ }
276
+ return targetPath
277
+ } catch (error) {
278
+ lastError = error
279
+ await handle?.close().catch(() => {})
280
+ await fs.promises.rm(targetPath, { force: true }).catch(() => {})
281
+ const caught = /** @type {any} */ (error)
282
+ const status = Number(caught?.status || 0)
283
+ const retryable = !(error instanceof ThemeMarketClientError)
284
+ || error.code === 'theme_download_unavailable' && ([408, 425, 429].includes(status) || status >= 500)
285
+ if (!retryable || attempt + 1 >= DOWNLOAD_ATTEMPTS) throw error
286
+ const rawRetryAfter = caught?.retryAfter
287
+ const delay = rawRetryAfter && Number.isFinite(Number(rawRetryAfter))
288
+ ? Math.min(Number(rawRetryAfter) * 1000, 15_000)
289
+ : 250 * (2 ** attempt) + Math.floor(Math.random() * 150)
290
+ await sleep(delay)
291
+ }
292
+ }
293
+ throw lastError || new ThemeMarketClientError('theme_download_unavailable')
294
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import YAML from 'yaml'
4
+ import { pluginResources } from '../filesystem/path.js'
5
+
6
+ export const themesDir = path.join(pluginResources, 'themes')
7
+
8
+ const legacyThemeDirs = [
9
+ path.join(pluginResources, 'html', 'b19', 'res', 'themes'),
10
+ path.join(pluginResources, 'html', 'b19', 'themes'),
11
+ ]
12
+ const THEME_ID_RE = /^[a-zA-Z0-9_-]+$/
13
+ const RESERVED_IDS = new Set(['default', 'snow', 'star', 'dss2', 'topText', 'foolsDay'])
14
+
15
+ /**
16
+ * Move themes from historical resource locations and normalize each folder to
17
+ * its declared theme id. Existing destinations are never overwritten.
18
+ * @returns {{from:string,to:string}[]}
19
+ */
20
+ export function migrateLegacyThemeDirectories() {
21
+ fs.mkdirSync(themesDir, { recursive: true, mode: 0o700 })
22
+ /** @type {{from:string,to:string}[]} */
23
+ const moved = []
24
+ for (const root of [...legacyThemeDirs, themesDir]) {
25
+ if (!fs.existsSync(root)) continue
26
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
27
+ if (!entry.isDirectory() || entry.name.startsWith('.phi-market-')) continue
28
+ const source = path.join(root, entry.name)
29
+ let id = ''
30
+ try {
31
+ const info = YAML.parse(fs.readFileSync(path.join(source, 'info.yaml'), 'utf8'))
32
+ id = typeof info?.id === 'string' ? info.id : ''
33
+ } catch { continue }
34
+ if (!THEME_ID_RE.test(id) || RESERVED_IDS.has(id)) continue
35
+ const destination = path.join(themesDir, id)
36
+ if (path.resolve(source) === path.resolve(destination) || fs.existsSync(destination)) continue
37
+ fs.renameSync(source, destination)
38
+ moved.push({ from: source, to: destination })
39
+ }
40
+ }
41
+ return moved
42
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { dataPath } from '../filesystem/path.js'
4
+
5
+ const SLUG_RE = /^[a-z][a-z0-9_-]{0,119}$/
6
+
7
+ export class ThemePolicyCache {
8
+ /** @param {string} filePath */
9
+ constructor(filePath) {
10
+ this.filePath = filePath
11
+ this.mode = 'blacklist'
12
+ /** @type {Set<string>} */
13
+ this.entries = new Set()
14
+ this.load()
15
+ }
16
+
17
+ load() {
18
+ try {
19
+ const value = JSON.parse(fs.readFileSync(this.filePath, 'utf8'))
20
+ this.apply(value, false)
21
+ } catch { /* empty blacklist is the safe, documented default */ }
22
+ }
23
+
24
+ /** @param {any} value @param {boolean} [persist=true] */
25
+ apply(value, persist = true) {
26
+ if (!value || !['blacklist', 'whitelist'].includes(value.mode) || !Array.isArray(value.entries)) return false
27
+ const entries = value.entries
28
+ .filter((/** @type {unknown} */ entry) => typeof entry === 'string' && SLUG_RE.test(entry))
29
+ .slice(0, 500)
30
+ this.mode = value.mode
31
+ this.entries = new Set(entries)
32
+ if (persist) this.save()
33
+ return true
34
+ }
35
+
36
+ save() {
37
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true })
38
+ const temporary = `${this.filePath}.${process.pid}.tmp`
39
+ fs.writeFileSync(temporary, JSON.stringify({ mode: this.mode, entries: [...this.entries].sort() }), { mode: 0o600 })
40
+ fs.renameSync(temporary, this.filePath)
41
+ }
42
+
43
+ /** @param {string} slug */
44
+ isAllowed(slug) {
45
+ const contains = this.entries.has(slug)
46
+ return this.mode === 'blacklist' ? !contains : contains
47
+ }
48
+
49
+ snapshot() {
50
+ return { mode: this.mode, entries: [...this.entries] }
51
+ }
52
+ }
53
+
54
+ export default new ThemePolicyCache(path.join(dataPath, 'theme-policy.json'))
@@ -0,0 +1,167 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import YAML from 'yaml'
5
+ import { migrateLegacyThemeDirectories, themesDir } from './paths.js'
6
+
7
+ const WORK_DIR = path.join(themesDir, '.phi-market-work')
8
+ const WORK_STALE_MS = 60 * 60_000
9
+ const UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
10
+
11
+ /** @param {string} receiptPath */
12
+ export async function readMarketReceipt(receiptPath) {
13
+ try {
14
+ const value = JSON.parse(await fs.promises.readFile(receiptPath, 'utf8'))
15
+ if (value?.source !== 'phi-theme-marketplace' || typeof value?.slug !== 'string'
16
+ || typeof value?.version !== 'string' || !/^[a-f0-9]{64}$/.test(value?.sha256)) return null
17
+ return value
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /** @param {string} value */
24
+ const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
+
26
+ /** @param {string} themeId @param {string} entryName */
27
+ function isThemeBackupName(themeId, entryName) {
28
+ return new RegExp(`^\\.phi-market-backup-${escapeRegExp(themeId)}-${UUID_PATTERN}$`, 'i').test(entryName)
29
+ }
30
+
31
+ /** @param {string} entryName */
32
+ export function getBackupThemeId(entryName) {
33
+ const match = new RegExp(`^\\.phi-market-backup-([a-z][a-z0-9_-]{0,119})-${UUID_PATTERN}$`, 'i').exec(entryName)
34
+ return match?.[1] || null
35
+ }
36
+
37
+ /**
38
+ * @param {string} directory
39
+ * @param {string} themeId
40
+ * @returns {Promise<{exists:boolean,healthy:boolean,marketOwned:boolean,receipt:any}>}
41
+ */
42
+ async function inspectMarketThemeDirectory(directory, themeId) {
43
+ const stat = await fs.promises.lstat(directory).catch(() => null)
44
+ if (!stat) return { exists: false, healthy: false, marketOwned: false, receipt: null }
45
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
46
+ return { exists: true, healthy: false, marketOwned: false, receipt: null }
47
+ }
48
+
49
+ const receiptPath = path.join(directory, '.phi-market.json')
50
+ const receiptMarker = await fs.promises.lstat(receiptPath).catch(() => null)
51
+ const receipt = await readMarketReceipt(receiptPath)
52
+ let info = null
53
+ try {
54
+ info = YAML.parse(await fs.promises.readFile(path.join(directory, 'info.yaml'), 'utf8'))
55
+ } catch { }
56
+ return {
57
+ exists: true,
58
+ healthy: info?.id === themeId && receipt?.slug === themeId,
59
+ // This marker is reserved for marketplace installs. A truncated marker
60
+ // still distinguishes an interrupted install from a same-name local theme.
61
+ marketOwned: Boolean(receiptMarker?.isFile()),
62
+ receipt,
63
+ }
64
+ }
65
+
66
+ /** @param {boolean} [force] @param {string} [workDirectory] */
67
+ export async function cleanStaleMarketWork(force = false, workDirectory = WORK_DIR) {
68
+ const now = Date.now()
69
+ const entries = await fs.promises.readdir(workDirectory, { withFileTypes: true }).catch(error => {
70
+ if (/** @type {any} */ (error)?.code === 'ENOENT') return []
71
+ throw error
72
+ })
73
+ for (const entry of entries) {
74
+ const fullPath = path.join(workDirectory, entry.name)
75
+ const stat = await fs.promises.stat(fullPath).catch(() => null)
76
+ if (stat && (force || now - stat.mtimeMs > WORK_STALE_MS)) {
77
+ await fs.promises.rm(fullPath, { recursive: true, force: true })
78
+ }
79
+ }
80
+ }
81
+
82
+ /** @param {string} themeId */
83
+ export async function recoverMarketInstall(themeId) {
84
+ migrateLegacyThemeDirectories()
85
+ await fs.promises.mkdir(WORK_DIR, { recursive: true, mode: 0o700 })
86
+ const target = path.join(themesDir, themeId)
87
+ const entries = await fs.promises.readdir(themesDir, { withFileTypes: true })
88
+ const backups = entries.filter(entry => entry.isDirectory() && isThemeBackupName(themeId, entry.name))
89
+ const candidates = await Promise.all(backups.map(async entry => {
90
+ const fullPath = path.join(themesDir, entry.name)
91
+ return {
92
+ fullPath,
93
+ stat: await fs.promises.stat(fullPath),
94
+ state: await inspectMarketThemeDirectory(fullPath, themeId),
95
+ }
96
+ }))
97
+ const healthyBackups = candidates
98
+ .filter(item => item.state.healthy)
99
+ .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
100
+ let targetState = await inspectMarketThemeDirectory(target, themeId)
101
+ let recovered = false
102
+
103
+ if ((!targetState.exists || targetState.marketOwned) && !targetState.healthy && healthyBackups.length) {
104
+ const recoverable = healthyBackups[0]
105
+ let displaced = ''
106
+ if (targetState.exists) {
107
+ displaced = path.join(WORK_DIR, `corrupt-${themeId}-${crypto.randomUUID()}`)
108
+ await fs.promises.rename(target, displaced)
109
+ }
110
+ try {
111
+ await fs.promises.rename(recoverable.fullPath, target)
112
+ targetState = await inspectMarketThemeDirectory(target, themeId)
113
+ if (!targetState.healthy) throw new Error(`Recovered theme ${themeId} failed validation`)
114
+ recovered = true
115
+ if (displaced) await fs.promises.rm(displaced, { recursive: true, force: true })
116
+ } catch (error) {
117
+ const restoredBackup = await fs.promises.lstat(target).catch(() => null)
118
+ if (restoredBackup) await fs.promises.rename(target, recoverable.fullPath).catch(() => {})
119
+ if (displaced) await fs.promises.rename(displaced, target).catch(() => {})
120
+ throw error
121
+ }
122
+ }
123
+
124
+ // Never discard a backup until a healthy marketplace target is present.
125
+ // A same-name local theme blocks recovery but does not destroy the backup.
126
+ if (targetState.healthy) {
127
+ for (const candidate of candidates) {
128
+ await fs.promises.rm(candidate.fullPath, { recursive: true, force: true })
129
+ }
130
+ }
131
+ return { recovered, healthy: targetState.healthy }
132
+ }
133
+
134
+ /**
135
+ * Recover every interrupted directory swap before the first theme scan.
136
+ * `cleanAllWork` is safe only while the global install lock is held.
137
+ * @param {{cleanAllWork?:boolean}} [options]
138
+ */
139
+ export async function recoverAllMarketInstalls(options = {}) {
140
+ await fs.promises.mkdir(themesDir, { recursive: true, mode: 0o700 })
141
+ const entries = await fs.promises.readdir(themesDir, { withFileTypes: true })
142
+ const themeIds = new Set(entries
143
+ .filter(entry => entry.isDirectory())
144
+ .map(entry => getBackupThemeId(entry.name))
145
+ .filter(Boolean))
146
+ /** @type {{themeId:string,error:unknown}[]} */
147
+ const failures = []
148
+ for (const themeId of themeIds) {
149
+ try {
150
+ await recoverMarketInstall(/** @type {string} */ (themeId))
151
+ } catch (error) {
152
+ failures.push({ themeId: /** @type {string} */ (themeId), error })
153
+ }
154
+ }
155
+ try {
156
+ await cleanStaleMarketWork(options.cleanAllWork === true)
157
+ if (options.cleanAllWork) {
158
+ for (const entry of entries) {
159
+ if (!entry.isDirectory() || !/^\.phi-market-restore-[a-zA-Z0-9]+$/.test(entry.name)) continue
160
+ await fs.promises.rm(path.join(themesDir, entry.name), { recursive: true, force: true })
161
+ }
162
+ }
163
+ } catch (error) {
164
+ failures.push({ themeId: '.phi-market-work', error })
165
+ }
166
+ return failures
167
+ }