@serifpersia/dango 3.6.5 → 3.6.8

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 (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -905
  66. package/server/dist/hono/tv.js +5 -169
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -307
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,385 +1 @@
1
- import WebSocket from 'ws';
2
- import logger from './logger.js';
3
- import { CONFIG } from './config.js';
4
- import dotenv from 'dotenv';
5
- const log = logger.child({ module: 'DiscordGateway' });
6
- class DiscordGatewayService {
7
- gateway = null;
8
- heartbeat = null;
9
- token = null;
10
- reconnectTimeout = null;
11
- lastActivity = null;
12
- isEnabled = false;
13
- RECONNECT_DELAY = 15000;
14
- externalAssetCache = new Map();
15
- setEnabled(enabled) {
16
- this.isEnabled = enabled;
17
- if (enabled) {
18
- this.loadToken();
19
- this.connect();
20
- }
21
- else {
22
- this.disconnect();
23
- }
24
- }
25
- get serviceEnabled() {
26
- return this.isEnabled && !!this.token && this.token.length > 30;
27
- }
28
- hasToken() {
29
- return !!this.token && this.token.length > 30;
30
- }
31
- loadToken() {
32
- this.token = process.env.DISCORD_GATEWAY_TOKEN || null;
33
- }
34
- reloadToken() {
35
- dotenv.config({ path: CONFIG.ENV_PATH, override: true });
36
- this.loadToken();
37
- if (this.serviceEnabled) {
38
- this.disconnect();
39
- this.connect();
40
- }
41
- }
42
- async getExternalAsset(url) {
43
- if (!CONFIG.DISCORD_CLIENT_ID || !this.token)
44
- return null;
45
- const cached = this.externalAssetCache.get(url);
46
- if (cached)
47
- return cached;
48
- try {
49
- const res = await fetch(`https://discord.com/api/v9/applications/${CONFIG.DISCORD_CLIENT_ID}/external-assets`, {
50
- method: 'POST',
51
- headers: {
52
- Authorization: this.token,
53
- 'Content-Type': 'application/json',
54
- },
55
- body: JSON.stringify({ urls: [url] }),
56
- });
57
- const text = await res.text();
58
- if (!res.ok) {
59
- log.warn(`external-assets proxy failed ${res.status}: ${text.slice(0, 200)}`);
60
- return null;
61
- }
62
- const data = JSON.parse(text);
63
- if (!data[0]?.external_asset_path)
64
- return null;
65
- const mp = `mp:${data[0].external_asset_path}`;
66
- this.externalAssetCache.set(url, mp);
67
- return mp;
68
- }
69
- catch (err) {
70
- log.warn({ err }, 'external-assets proxy error');
71
- return null;
72
- }
73
- }
74
- resolveImageUrl(data) {
75
- const isSafeUrl = (url) => {
76
- if (!url.startsWith('https://'))
77
- return false;
78
- if (url.includes('localhost') || url.includes('127.0.0.1'))
79
- return false;
80
- if (url.includes('s4.anilist.co') || url.includes('anilistcdn'))
81
- return true;
82
- const blocked = [
83
- 'youtube-anime.com',
84
- 'animepahe',
85
- 'animeya.cc',
86
- 'gogocdn.net',
87
- 'weeabo0.xyz',
88
- 'japaneseasmr.com',
89
- ];
90
- return !blocked.some((d) => url.includes(d));
91
- };
92
- let thumb = data.thumbnail;
93
- if (thumb && thumb.includes('/api/image-proxy')) {
94
- const m = thumb.match(/url=([^&]+)/);
95
- if (m) {
96
- try {
97
- thumb = decodeURIComponent(m[1]);
98
- }
99
- catch (_e) {
100
- void _e;
101
- }
102
- }
103
- }
104
- if (thumb && data.providerName !== 'AnimePahe' && isSafeUrl(thumb))
105
- return thumb;
106
- if (data.thumbnails) {
107
- const safe = data.thumbnails.find((t) => t && (t.includes('s4.anilist.co') || t.includes('anilistcdn')));
108
- if (safe)
109
- return safe;
110
- }
111
- if (thumb && isSafeUrl(thumb))
112
- return thumb;
113
- return null;
114
- }
115
- async updatePresence(data) {
116
- this.lastActivity = data;
117
- if (!this.isEnabled || !this.gateway || this.gateway.readyState !== WebSocket.OPEN)
118
- return;
119
- const name = 'dango';
120
- const details = data.title;
121
- const state = data.stateLine
122
- ? data.stateLine
123
- : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''}`;
124
- const type = data.providerName === 'ASMR' || data.providerName === 'Radio' || data.providerName === 'Music'
125
- ? 2
126
- : 3;
127
- const imageUrl = this.resolveImageUrl(data);
128
- let assets;
129
- if (imageUrl) {
130
- if (CONFIG.DISCORD_CLIENT_ID) {
131
- const proxied = await this.getExternalAsset(imageUrl);
132
- if (proxied) {
133
- assets = {
134
- large_image: proxied,
135
- large_text: data.title.slice(0, 128),
136
- small_image: 'small',
137
- small_text: 'dango',
138
- };
139
- }
140
- else {
141
- assets = { large_image: 'logo', large_text: data.title.slice(0, 128) };
142
- }
143
- }
144
- }
145
- else if (CONFIG.DISCORD_CLIENT_ID) {
146
- assets = { large_image: 'logo', large_text: data.title.slice(0, 128) };
147
- }
148
- const presence = {
149
- since: null,
150
- activities: [
151
- {
152
- name,
153
- type,
154
- state: data.isPlaying ? state : `${state} (Paused)`,
155
- details,
156
- ...(CONFIG.DISCORD_CLIENT_ID ? { application_id: CONFIG.DISCORD_CLIENT_ID } : {}),
157
- ...(assets ? { assets } : {}),
158
- buttons: ['Learn More'],
159
- metadata: { button_urls: ['https://github.com/serifpersia/dango'] },
160
- status_display_type: 2,
161
- },
162
- ],
163
- status: 'online',
164
- afk: false,
165
- };
166
- const nowMs = Date.now();
167
- if (data.isPlaying) {
168
- if (data.currentTime && data.currentTime > 0) {
169
- presence.since = nowMs - Math.round(data.currentTime * 1000);
170
- }
171
- if (data.duration && data.duration > data.currentTime) {
172
- presence.activities[0].timestamps = {
173
- start: nowMs - Math.round(data.currentTime * 1000),
174
- end: nowMs + Math.round((data.duration - data.currentTime) * 1000),
175
- };
176
- }
177
- }
178
- try {
179
- this.gateway.send(JSON.stringify({ op: 3, d: presence }));
180
- }
181
- catch (err) {
182
- log.error({ err }, 'Failed to send gateway presence update');
183
- }
184
- }
185
- setIdleStatus(page) {
186
- if (!this.isEnabled || !this.gateway || this.gateway.readyState !== WebSocket.OPEN)
187
- return;
188
- const pageLabels = {
189
- home: { details: 'Home', state: 'Browsing anime' },
190
- search: { details: 'Search', state: 'Exploring titles...' },
191
- watchlist: { details: 'Watchlist', state: 'Reviewing the watchlist' },
192
- anime: { details: 'Anime Info', state: 'Reading show details' },
193
- insights: { details: 'Insights', state: 'Reviewing stats' },
194
- settings: { details: 'Settings', state: 'Tweaking preferences' },
195
- trackers: { details: 'Trackers', state: 'Syncing watchlists' },
196
- map: { details: 'Map', state: 'Exploring the global user map' },
197
- asmr: { details: 'ASMR', state: 'Browsing ASMR works' },
198
- radio: { details: 'Radio', state: 'Browsing radio stations' },
199
- music: { details: 'Music', state: 'Browsing music' },
200
- tv: { details: 'TV', state: 'Browsing movies & shows' },
201
- manga: { details: 'Manga', state: 'Browsing manga' },
202
- };
203
- const label = pageLabels[page] ?? { details: 'dango', state: 'Idle' };
204
- try {
205
- this.gateway.send(JSON.stringify({
206
- op: 3,
207
- d: {
208
- since: null,
209
- activities: [
210
- {
211
- name: 'dango',
212
- type: page === 'asmr' || page === 'radio' || page === 'music' ? 2 : 3,
213
- state: label.state,
214
- details: label.details,
215
- ...(CONFIG.DISCORD_CLIENT_ID ? { application_id: CONFIG.DISCORD_CLIENT_ID } : {}),
216
- ...(CONFIG.DISCORD_CLIENT_ID
217
- ? { assets: { large_image: 'logo', large_text: 'dango' } }
218
- : {}),
219
- buttons: ['Learn More'],
220
- metadata: { button_urls: ['https://github.com/serifpersia/dango'] },
221
- status_display_type: 2,
222
- },
223
- ],
224
- status: 'online',
225
- afk: false,
226
- },
227
- }));
228
- }
229
- catch (err) {
230
- log.error({ err }, 'Failed to send gateway idle status');
231
- }
232
- }
233
- clearPresence() {
234
- if (!this.isEnabled)
235
- return;
236
- if (this.gateway && this.gateway.readyState === WebSocket.OPEN) {
237
- try {
238
- this.gateway.send(JSON.stringify({
239
- op: 3,
240
- d: { since: null, activities: [], status: 'online', afk: false },
241
- }));
242
- }
243
- catch {
244
- // ignore
245
- }
246
- }
247
- this.lastActivity = null;
248
- }
249
- shutdown() {
250
- this.isEnabled = false;
251
- this.token = null;
252
- this.disconnect();
253
- }
254
- disconnect() {
255
- if (this.reconnectTimeout) {
256
- clearTimeout(this.reconnectTimeout);
257
- this.reconnectTimeout = null;
258
- }
259
- if (this.gateway) {
260
- try {
261
- if (this.gateway.readyState === WebSocket.OPEN) {
262
- this.gateway.send(JSON.stringify({
263
- op: 3,
264
- d: { since: null, activities: [], status: 'online', afk: false },
265
- }));
266
- }
267
- this.gateway.close();
268
- }
269
- catch {
270
- // ignore
271
- }
272
- this.gateway = null;
273
- }
274
- if (this.heartbeat) {
275
- clearInterval(this.heartbeat);
276
- this.heartbeat = null;
277
- }
278
- }
279
- connect() {
280
- if (!this.isEnabled || !this.token || this.gateway)
281
- return;
282
- const wsUrl = 'wss://gateway.discord.gg/?encoding=json&v=10';
283
- log.debug('Connecting Discord gateway');
284
- this.gateway = new WebSocket(wsUrl);
285
- let heartbeatInterval = 41250;
286
- this.gateway.on('message', (data) => {
287
- try {
288
- const msg = JSON.parse(data.toString());
289
- if (msg.op === 10) {
290
- heartbeatInterval = msg.d.heartbeat_interval;
291
- this.heartbeat = setInterval(() => {
292
- try {
293
- if (this.gateway && this.gateway.readyState === WebSocket.OPEN) {
294
- this.gateway.send(JSON.stringify({ op: 1, d: null }));
295
- }
296
- }
297
- catch {
298
- // ignore
299
- }
300
- }, heartbeatInterval);
301
- const identify = {
302
- op: 2,
303
- d: {
304
- token: this.token,
305
- capabilities: 1021,
306
- client_state: {
307
- guild_hashes: {},
308
- highest_last_message_id: '0',
309
- private_channels_version: '0',
310
- read_state_version: 0,
311
- user_guild_settings_version: -1,
312
- user_settings_version: -1,
313
- },
314
- compress: false,
315
- presence: {
316
- since: null,
317
- activities: [],
318
- status: 'online',
319
- afk: false,
320
- },
321
- properties: {
322
- os: 'Windows',
323
- os_version: '10.0.19044',
324
- os_arch: 'x64',
325
- browser: 'Discord Client',
326
- client_version: '0.0.20',
327
- client_build_number: 152131,
328
- client_event_source: null,
329
- release_channel: 'stable',
330
- system_locale: 'en-US',
331
- },
332
- },
333
- };
334
- if (this.gateway) {
335
- this.gateway.send(JSON.stringify(identify));
336
- }
337
- }
338
- else if (msg.op === 11) {
339
- // ignore
340
- }
341
- else if (msg.op === 0) {
342
- if (msg.t === 'READY') {
343
- log.info(`Gateway READY as ${msg.d.user?.username || 'unknown'}`);
344
- if (this.lastActivity) {
345
- void this.updatePresence(this.lastActivity).catch((err) => log.warn({ err }, 'failed to restore presence after READY'));
346
- }
347
- }
348
- }
349
- else if (msg.op === 7) {
350
- log.warn('Gateway reconnect requested by Discord');
351
- this.handleDisconnect();
352
- }
353
- else if (msg.op === 9) {
354
- log.warn('Gateway invalid session, reconnecting...');
355
- this.handleDisconnect();
356
- }
357
- }
358
- catch (err) {
359
- log.debug({ err }, 'Gateway message parse error');
360
- }
361
- });
362
- this.gateway.on('close', (code, reason) => {
363
- log.warn(`Gateway closed ${code} ${reason.toString().slice(0, 100)}`);
364
- this.handleDisconnect();
365
- });
366
- this.gateway.on('error', (err) => {
367
- log.error(err, 'Gateway WebSocket error');
368
- this.handleDisconnect();
369
- });
370
- }
371
- handleDisconnect() {
372
- if (this.heartbeat) {
373
- clearInterval(this.heartbeat);
374
- this.heartbeat = null;
375
- }
376
- this.gateway = null;
377
- if (this.isEnabled && this.token) {
378
- this.reconnectTimeout = setTimeout(() => {
379
- this.reconnectTimeout = null;
380
- this.connect();
381
- }, this.RECONNECT_DELAY);
382
- }
383
- }
384
- }
385
- export const discordGatewayService = new DiscordGatewayService();
1
+ import o from"ws";import d from"./logger.js";import{CONFIG as r}from"./config.js";import m from"dotenv";const a=d.child({module:"DiscordGateway"});class p{gateway=null;heartbeat=null;token=null;reconnectTimeout=null;lastActivity=null;isEnabled=!1;RECONNECT_DELAY=15e3;externalAssetCache=new Map;setEnabled(e){this.isEnabled=e,e?(this.loadToken(),this.connect()):this.disconnect()}get serviceEnabled(){return this.isEnabled&&!!this.token&&this.token.length>30}hasToken(){return!!this.token&&this.token.length>30}loadToken(){this.token=process.env.DISCORD_GATEWAY_TOKEN||null}reloadToken(){m.config({path:r.ENV_PATH,override:!0}),this.loadToken(),this.serviceEnabled&&(this.disconnect(),this.connect())}async getExternalAsset(e){if(!r.DISCORD_CLIENT_ID||!this.token)return null;const n=this.externalAssetCache.get(e);if(n)return n;try{const s=await fetch(`https://discord.com/api/v9/applications/${r.DISCORD_CLIENT_ID}/external-assets`,{method:"POST",headers:{Authorization:this.token,"Content-Type":"application/json"},body:JSON.stringify({urls:[e]})}),t=await s.text();if(!s.ok)return a.warn(`external-assets proxy failed ${s.status}: ${t.slice(0,200)}`),null;const i=JSON.parse(t);if(!i[0]?.external_asset_path)return null;const l=`mp:${i[0].external_asset_path}`;return this.externalAssetCache.set(e,l),l}catch(s){return a.warn({err:s},"external-assets proxy error"),null}}resolveImageUrl(e){const n=t=>!t.startsWith("https://")||t.includes("localhost")||t.includes("127.0.0.1")?!1:t.includes("s4.anilist.co")||t.includes("anilistcdn")?!0:!["youtube-anime.com","animepahe","animeya.cc","gogocdn.net","weeabo0.xyz","japaneseasmr.com"].some(l=>t.includes(l));let s=e.thumbnail;if(s&&s.includes("/api/image-proxy")){const t=s.match(/url=([^&]+)/);if(t)try{s=decodeURIComponent(t[1])}catch(i){}}if(s&&e.providerName!=="AnimePahe"&&n(s))return s;if(e.thumbnails){const t=e.thumbnails.find(i=>i&&(i.includes("s4.anilist.co")||i.includes("anilistcdn")));if(t)return t}return s&&n(s)?s:null}async updatePresence(e){if(this.lastActivity=e,!this.isEnabled||!this.gateway||this.gateway.readyState!==o.OPEN)return;const n="dango",s=e.title,t=e.stateLine?e.stateLine:`Episode ${e.episode}${e.totalEpisodes?`/${e.totalEpisodes}`:""}`,i=e.providerName==="ASMR"||e.providerName==="Radio"||e.providerName==="Music"?2:3,l=this.resolveImageUrl(e);let c;if(l){if(r.DISCORD_CLIENT_ID){const g=await this.getExternalAsset(l);g?c={large_image:g,large_text:e.title.slice(0,128),small_image:"small",small_text:"dango"}:c={large_image:"logo",large_text:e.title.slice(0,128)}}}else r.DISCORD_CLIENT_ID&&(c={large_image:"logo",large_text:e.title.slice(0,128)});const h={since:null,activities:[{name:n,type:i,state:e.isPlaying?t:`${t} (Paused)`,details:s,...r.DISCORD_CLIENT_ID?{application_id:r.DISCORD_CLIENT_ID}:{},...c?{assets:c}:{},buttons:["Learn More"],metadata:{button_urls:["https://github.com/serifpersia/dango"]},status_display_type:2}],status:"online",afk:!1},u=Date.now();e.isPlaying&&(e.currentTime&&e.currentTime>0&&(h.since=u-Math.round(e.currentTime*1e3)),e.duration&&e.duration>e.currentTime&&(h.activities[0].timestamps={start:u-Math.round(e.currentTime*1e3),end:u+Math.round((e.duration-e.currentTime)*1e3)}));try{this.gateway.send(JSON.stringify({op:3,d:h}))}catch(g){a.error({err:g},"Failed to send gateway presence update")}}setIdleStatus(e){if(!this.isEnabled||!this.gateway||this.gateway.readyState!==o.OPEN)return;const s={home:{details:"Home",state:"Browsing anime"},search:{details:"Search",state:"Exploring titles..."},watchlist:{details:"Watchlist",state:"Reviewing the watchlist"},anime:{details:"Anime Info",state:"Reading show details"},insights:{details:"Insights",state:"Reviewing stats"},settings:{details:"Settings",state:"Tweaking preferences"},trackers:{details:"Trackers",state:"Syncing watchlists"},map:{details:"Map",state:"Exploring the global user map"},asmr:{details:"ASMR",state:"Browsing ASMR works"},radio:{details:"Radio",state:"Browsing radio stations"},music:{details:"Music",state:"Browsing music"},tv:{details:"TV",state:"Browsing movies & shows"},manga:{details:"Manga",state:"Browsing manga"}}[e]??{details:"dango",state:"Idle"};try{this.gateway.send(JSON.stringify({op:3,d:{since:null,activities:[{name:"dango",type:e==="asmr"||e==="radio"||e==="music"?2:3,state:s.state,details:s.details,...r.DISCORD_CLIENT_ID?{application_id:r.DISCORD_CLIENT_ID}:{},...r.DISCORD_CLIENT_ID?{assets:{large_image:"logo",large_text:"dango"}}:{},buttons:["Learn More"],metadata:{button_urls:["https://github.com/serifpersia/dango"]},status_display_type:2}],status:"online",afk:!1}}))}catch(t){a.error({err:t},"Failed to send gateway idle status")}}clearPresence(){if(this.isEnabled){if(this.gateway&&this.gateway.readyState===o.OPEN)try{this.gateway.send(JSON.stringify({op:3,d:{since:null,activities:[],status:"online",afk:!1}}))}catch{}this.lastActivity=null}}shutdown(){this.isEnabled=!1,this.token=null,this.disconnect()}disconnect(){if(this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),this.gateway){try{this.gateway.readyState===o.OPEN&&this.gateway.send(JSON.stringify({op:3,d:{since:null,activities:[],status:"online",afk:!1}})),this.gateway.close()}catch{}this.gateway=null}this.heartbeat&&(clearInterval(this.heartbeat),this.heartbeat=null)}connect(){if(!this.isEnabled||!this.token||this.gateway)return;const e="wss://gateway.discord.gg/?encoding=json&v=10";a.debug("Connecting Discord gateway"),this.gateway=new o(e);let n=41250;this.gateway.on("message",s=>{try{const t=JSON.parse(s.toString());if(t.op===10){n=t.d.heartbeat_interval,this.heartbeat=setInterval(()=>{try{this.gateway&&this.gateway.readyState===o.OPEN&&this.gateway.send(JSON.stringify({op:1,d:null}))}catch{}},n);const i={op:2,d:{token:this.token,capabilities:1021,client_state:{guild_hashes:{},highest_last_message_id:"0",private_channels_version:"0",read_state_version:0,user_guild_settings_version:-1,user_settings_version:-1},compress:!1,presence:{since:null,activities:[],status:"online",afk:!1},properties:{os:"Windows",os_version:"10.0.19044",os_arch:"x64",browser:"Discord Client",client_version:"0.0.20",client_build_number:152131,client_event_source:null,release_channel:"stable",system_locale:"en-US"}}};this.gateway&&this.gateway.send(JSON.stringify(i))}else t.op===11||(t.op===0?t.t==="READY"&&(a.info(`Gateway READY as ${t.d.user?.username||"unknown"}`),this.lastActivity&&this.updatePresence(this.lastActivity).catch(i=>a.warn({err:i},"failed to restore presence after READY"))):t.op===7?(a.warn("Gateway reconnect requested by Discord"),this.handleDisconnect()):t.op===9&&(a.warn("Gateway invalid session, reconnecting..."),this.handleDisconnect()))}catch(t){a.debug({err:t},"Gateway message parse error")}}),this.gateway.on("close",(s,t)=>{a.warn(`Gateway closed ${s} ${t.toString().slice(0,100)}`),this.handleDisconnect()}),this.gateway.on("error",s=>{a.error(s,"Gateway WebSocket error"),this.handleDisconnect()})}handleDisconnect(){this.heartbeat&&(clearInterval(this.heartbeat),this.heartbeat=null),this.gateway=null,this.isEnabled&&this.token&&(this.reconnectTimeout=setTimeout(()=>{this.reconnectTimeout=null,this.connect()},this.RECONNECT_DELAY))}}const v=new p;export{v as discordGatewayService};