@gooin/garmin-connect 1.6.12 → 1.7.2

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.
@@ -10,79 +10,60 @@ const lodash_1 = __importDefault(require("lodash"));
10
10
  const luxon_1 = require("luxon");
11
11
  const oauth_1_0a_1 = __importDefault(require("oauth-1.0a"));
12
12
  const qs_1 = __importDefault(require("qs"));
13
+ const MFAManager_1 = require("./MFAManager");
13
14
  const node_crypto_1 = __importDefault(require("node:crypto"));
15
+ const tough_cookie_1 = require("tough-cookie");
16
+ const axios_cookiejar_support_1 = require("axios-cookiejar-support");
17
+ // 正则表达式常量
14
18
  const CSRF_RE = new RegExp('name="_csrf"\\s+value="(.+?)"');
15
19
  const TICKET_RE = new RegExp('ticket=([^"]+)"');
16
20
  const ACCOUNT_LOCKED_RE = new RegExp('var statuss*=s*"([^"]*)"');
17
21
  const PAGE_TITLE_RE = new RegExp('<title>([^<]*)</title>');
22
+ // 用户代理常量
18
23
  const USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile';
19
24
  const USER_AGENT_BROWSER = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36';
20
25
  const USER_AGENT_BROWSER_MAC = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
26
+ // URL常量
21
27
  const OAUTH_CONSUMER_URL = 'https://thegarth.s3.amazonaws.com/oauth_consumer.json';
28
+ // HTTP状态码常量
22
29
  const HTTP_STATUS = {
23
30
  UNAUTHORIZED: 401
24
31
  };
32
+ // 全局变量
25
33
  let tokenRefreshPromise = null;
26
34
  let refreshSubscribers = [];
27
35
  class HttpClient {
28
36
  constructor(url, config) {
29
37
  var _a, _b;
38
+ const jar = new tough_cookie_1.CookieJar();
30
39
  this.url = url;
31
- this.client = axios_1.default.create({
40
+ this.client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
32
41
  timeout: (_a = config === null || config === void 0 ? void 0 : config.timeout) !== null && _a !== void 0 ? _a : 5000,
33
- timeoutErrorMessage: `Request Timeout: > ${(_b = config === null || config === void 0 ? void 0 : config.timeout) !== null && _b !== void 0 ? _b : 5000} ms`
34
- /**
35
- * Charles debugger: uncomment `proxy` and `httpsAgent`, then run bellow command.
36
- * NODE_TLS_REJECT_UNAUTHORIZED=0 node test/sync.js
37
- */
38
- // proxy: {
39
- // host: '127.0.0.1',
40
- // port: 8888,
41
- // protocol: 'http'
42
- // },
43
- // httpsAgent: new (require('https').Agent)({
44
- // rejectUnauthorized: false
45
- // })
46
- });
42
+ timeoutErrorMessage: `Request Timeout: > ${(_b = config === null || config === void 0 ? void 0 : config.timeout) !== null && _b !== void 0 ? _b : 5000} ms`,
43
+ maxRedirects: 10,
44
+ validateStatus: function (status) {
45
+ return status >= 200 && status < 400;
46
+ },
47
+ withCredentials: true,
48
+ jar: jar
49
+ }));
47
50
  this.config = config;
48
- this.client.interceptors.response.use((response) => response, async (error) => {
49
- var _a;
50
- if (axios_1.default.isAxiosError(error) &&
51
- error.code === 'ECONNABORTED') {
52
- throw new Error(error.message || 'Request Timeout');
53
- }
54
- const originalRequest = error.config;
55
- if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === HTTP_STATUS.UNAUTHORIZED &&
56
- !(originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest._retry)) {
57
- if (!this.oauth2Token) {
58
- throw new Error('No OAuth2 token available');
59
- }
60
- originalRequest._retry = true;
61
- try {
62
- if (!tokenRefreshPromise) {
63
- tokenRefreshPromise =
64
- this.refreshOauth2Token().finally(() => {
65
- tokenRefreshPromise = null;
66
- });
67
- }
68
- await tokenRefreshPromise;
69
- originalRequest.headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
70
- return this.client(originalRequest);
71
- }
72
- catch (err) {
73
- console.error('Token refresh failed:', err);
74
- throw err;
75
- }
76
- }
77
- if (axios_1.default.isAxiosError(error) && error.response) {
78
- this.handleError(error.response);
79
- }
80
- else {
81
- // 处理没有response的情况
82
- throw new Error('Network error or unknown error occurred');
83
- }
84
- throw error;
51
+ const mfaStorageDir = config.mfaStorageDir || './shared-mfa-sessions';
52
+ this.mfaManager = MFAManager_1.MFAManager.getInstance(mfaStorageDir);
53
+ this.setupInterceptors();
54
+ }
55
+ /**
56
+ * 设置请求和响应拦截器
57
+ */
58
+ setupInterceptors() {
59
+ // 响应拦截器
60
+ this.client.interceptors.response.use((response) => {
61
+ // this.logResponseTracking(response);
62
+ return response;
63
+ }, async (error) => {
64
+ return this.handleResponseError(error);
85
65
  });
66
+ // 请求拦截器
86
67
  this.client.interceptors.request.use(async (config) => {
87
68
  if (this.oauth2Token) {
88
69
  config.headers.Authorization =
@@ -91,6 +72,66 @@ class HttpClient {
91
72
  return config;
92
73
  });
93
74
  }
75
+ /**
76
+ * 记录响应跟踪信息
77
+ */
78
+ logResponseTracking(response) {
79
+ var _a, _b, _c, _d;
80
+ if (((_a = response.config.url) === null || _a === void 0 ? void 0 : _a.includes('signin')) ||
81
+ ((_b = response.config.url) === null || _b === void 0 ? void 0 : _b.includes('verifyMFA'))) {
82
+ console.log('> 响应跟踪 - URL:', response.config.url);
83
+ console.log('响应跟踪 - 状态码:', response.status);
84
+ console.log('响应跟踪 - 最终URL:', ((_c = response.request) === null || _c === void 0 ? void 0 : _c.responseURL) || response.config.url);
85
+ console.log('响应跟踪 - 重定向次数:', ((_d = response.request) === null || _d === void 0 ? void 0 : _d.redirectCount) || 0);
86
+ if (response.headers.location) {
87
+ console.log('响应跟踪 - Location头:', response.headers.location);
88
+ }
89
+ if (response.status >= 300 && response.status < 400) {
90
+ console.log('响应跟踪 - 检测到重定向状态码:', response.status);
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * 处理响应错误
96
+ */
97
+ async handleResponseError(error) {
98
+ var _a;
99
+ if (axios_1.default.isAxiosError(error) && error.code === 'ECONNABORTED') {
100
+ throw new Error(error.message || 'Request Timeout');
101
+ }
102
+ const originalRequest = error.config;
103
+ if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === HTTP_STATUS.UNAUTHORIZED &&
104
+ !(originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest._retry)) {
105
+ if (!this.oauth2Token) {
106
+ throw new Error('No OAuth2 token available');
107
+ }
108
+ originalRequest._retry = true;
109
+ try {
110
+ if (!tokenRefreshPromise) {
111
+ tokenRefreshPromise = this.refreshOauth2Token().finally(() => {
112
+ tokenRefreshPromise = null;
113
+ });
114
+ }
115
+ await tokenRefreshPromise;
116
+ originalRequest.headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
117
+ return this.client(originalRequest);
118
+ }
119
+ catch (err) {
120
+ console.error('Token refresh failed:', err);
121
+ throw err;
122
+ }
123
+ }
124
+ if (axios_1.default.isAxiosError(error) && error.response) {
125
+ this.handleError(error.response);
126
+ }
127
+ else {
128
+ throw new Error('Network error or unknown error occurred');
129
+ }
130
+ throw error;
131
+ }
132
+ /**
133
+ * 获取OAuth消费者信息
134
+ */
94
135
  async fetchOauthConsumer() {
95
136
  const response = await axios_1.default.get(OAUTH_CONSUMER_URL);
96
137
  this.OAUTH_CONSUMER = {
@@ -98,6 +139,9 @@ class HttpClient {
98
139
  secret: response.data.consumer_secret
99
140
  };
100
141
  }
142
+ /**
143
+ * 检查令牌有效性
144
+ */
101
145
  async checkTokenVaild() {
102
146
  if (this.oauth2Token) {
103
147
  if (this.oauth2Token.expires_at < luxon_1.DateTime.now().toSeconds()) {
@@ -106,18 +150,30 @@ class HttpClient {
106
150
  }
107
151
  }
108
152
  }
153
+ /**
154
+ * GET请求
155
+ */
109
156
  async get(url, config) {
110
157
  const response = await this.client.get(url, config);
111
158
  return response === null || response === void 0 ? void 0 : response.data;
112
159
  }
160
+ /**
161
+ * POST请求
162
+ */
113
163
  async post(url, data, config) {
114
164
  const response = await this.client.post(url, data, config);
115
165
  return response === null || response === void 0 ? void 0 : response.data;
116
166
  }
167
+ /**
168
+ * PUT请求
169
+ */
117
170
  async put(url, data, config) {
118
171
  const response = await this.client.put(url, data, config);
119
172
  return response === null || response === void 0 ? void 0 : response.data;
120
173
  }
174
+ /**
175
+ * DELETE请求
176
+ */
121
177
  async delete(url, config) {
122
178
  const response = await this.client.post(url, null, {
123
179
  ...config,
@@ -128,14 +184,23 @@ class HttpClient {
128
184
  });
129
185
  return response === null || response === void 0 ? void 0 : response.data;
130
186
  }
187
+ /**
188
+ * 设置通用请求头
189
+ */
131
190
  setCommonHeader(headers) {
132
191
  lodash_1.default.each(headers, (headerValue, key) => {
133
192
  this.client.defaults.headers.common[key] = headerValue;
134
193
  });
135
194
  }
195
+ /**
196
+ * 处理错误
197
+ */
136
198
  handleError(response) {
137
199
  this.handleHttpError(response);
138
200
  }
201
+ /**
202
+ * 处理HTTP错误
203
+ */
139
204
  handleHttpError(response) {
140
205
  const { status, statusText, data } = response;
141
206
  const errorMessage = {
@@ -147,134 +212,313 @@ class HttpClient {
147
212
  throw new Error(`HTTP Error (${status}): ${statusText}`);
148
213
  }
149
214
  /**
150
- * Login to Garmin Connect
151
- * @param username
152
- * @param password
153
- * @returns {Promise<HttpClient>}
154
- */
155
- async login(username, password) {
156
- await this.fetchOauthConsumer();
157
- // Step1-3: Get ticket from page.
158
- const ticket = await this.getLoginTicket(username, password);
159
- // Step4: Oauth1
160
- const oauth1 = await this.getOauth1Token(ticket);
161
- // TODO: Handle MFA
162
- // Step 5: Oauth2
163
- await this.exchange(oauth1);
164
- return this;
165
- }
166
- async getLoginTicket(username, password) {
167
- // Step1: Set cookie
168
- const step1Params = {
169
- clientId: 'GarminConnect',
170
- locale: 'en',
171
- service: this.url.GC_MODERN
215
+ * 登录到Garmin Connect
216
+ * @param username 用户名
217
+ * @param password 密码
218
+ * @param mfaCallback MFA验证回调函数
219
+ * @param sessionId 会话ID,用于分步登录
220
+ * @returns Promise<HttpClient>
221
+ */
222
+ async login(username, password, sessionId) {
223
+ try {
224
+ // 准备登录
225
+ await this.fetchOauthConsumer();
226
+ // 获取登录票据
227
+ const ticket = await this.getLoginTicket(username, password, sessionId);
228
+ // 获取OAuth1令牌
229
+ const oauth1 = await this.getOauth1Token(ticket);
230
+ // 交换OAuth2令牌
231
+ await this.exchange(oauth1);
232
+ return this;
233
+ }
234
+ catch (error) {
235
+ console.error('Login failed:', error);
236
+ throw error;
237
+ }
238
+ }
239
+ /**
240
+ * 获取登录票据
241
+ * @param username 用户名
242
+ * @param password 密码
243
+ * @param mfaCallback MFA验证回调函数
244
+ * @param sessionId 会话ID,用于分步登录
245
+ * @returns 登录票据
246
+ */
247
+ async getLoginTicket(username, password, sessionId) {
248
+ // 准备登录参数
249
+ const loginParams = this.prepareLoginParams();
250
+ // 步骤1: 设置cookie
251
+ await this.performLoginStep1(loginParams.step1Params);
252
+ // 步骤2: 获取CSRF令牌
253
+ const csrfToken = await this.performLoginStep2(loginParams.step2Params);
254
+ // 步骤3: 提交凭据
255
+ let signinResult = await this.performLoginStep3(username, password, csrfToken, loginParams.step3Params);
256
+ // 检查账户锁定状态
257
+ this.handleAccountLocked(signinResult);
258
+ // 检查页面标题,判断是否需要MFA
259
+ const pageTitle = this.handlePageTitle(signinResult);
260
+ // 如果需要MFA,执行MFA验证
261
+ if (this.isMFARequired(pageTitle)) {
262
+ // 如果提供了sessionId,则使用分步登录模式
263
+ if (sessionId) {
264
+ // 等待外部提供验证码
265
+ const mfaCode = await MFAManager_1.MFAManager.getInstance().waitForMFACode(sessionId);
266
+ // 使用获取到的验证码完成MFA验证
267
+ signinResult = await this.handleMFAWithCode(signinResult, loginParams.step3Params, mfaCode);
268
+ }
269
+ else {
270
+ throw new Error('需要MFA验证,但未提供验证码获取方式');
271
+ }
272
+ }
273
+ // 提取票据
274
+ const ticket = this.extractTicket(signinResult);
275
+ if (!ticket) {
276
+ throw new Error('登录失败(未找到票据或MFA验证失败),请检查用户名和密码');
277
+ }
278
+ return ticket;
279
+ }
280
+ /**
281
+ * 准备登录参数
282
+ */
283
+ prepareLoginParams() {
284
+ return {
285
+ step1Params: {
286
+ clientId: 'GarminConnect',
287
+ locale: 'en',
288
+ service: this.url.GC_MODERN
289
+ },
290
+ step2Params: {
291
+ id: 'gauth-widget',
292
+ embedWidget: true,
293
+ locale: 'en',
294
+ gauthHost: this.url.GARMIN_SSO_EMBED
295
+ },
296
+ step3Params: {
297
+ id: 'gauth-widget',
298
+ embedWidget: true,
299
+ clientId: 'GarminConnect',
300
+ locale: 'en',
301
+ gauthHost: this.url.GARMIN_SSO_EMBED,
302
+ service: this.url.GARMIN_SSO_EMBED,
303
+ source: this.url.GARMIN_SSO_EMBED,
304
+ redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
305
+ redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
306
+ }
172
307
  };
308
+ }
309
+ /**
310
+ * 执行登录步骤1:设置cookie
311
+ */
312
+ async performLoginStep1(step1Params) {
173
313
  const step1Url = `${this.url.GARMIN_SSO_EMBED}?${qs_1.default.stringify(step1Params)}`;
174
- // console.log('login - step1Url:', step1Url);
175
314
  await this.client.get(step1Url);
176
- // Step2 Get _csrf
177
- const step2Params = {
178
- id: 'gauth-widget',
179
- embedWidget: true,
180
- locale: 'en',
181
- gauthHost: this.url.GARMIN_SSO_EMBED
182
- };
315
+ }
316
+ /**
317
+ * 执行登录步骤2:获取CSRF令牌
318
+ */
319
+ async performLoginStep2(step2Params) {
183
320
  const step2Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(step2Params)}`;
184
- // console.log('login - step2Url:', step2Url);
185
321
  const step2Result = await this.get(step2Url);
186
- // console.log('login - step2Result:', step2Result)
187
- const csrfRegResult = CSRF_RE.exec(step2Result);
188
- if (!csrfRegResult) {
189
- throw new Error('login - csrf not found');
322
+ const csrfToken = this.extractCsrfToken(step2Result);
323
+ if (!csrfToken) {
324
+ throw new Error('登录 - 未找到CSRF令牌');
190
325
  }
191
- const csrf_token = csrfRegResult[1];
192
- // console.log('login - csrf:', csrf_token);
193
- // Step3 Get ticket
194
- const signinParams = {
195
- id: 'gauth-widget',
196
- embedWidget: true,
197
- clientId: 'GarminConnect',
198
- locale: 'en',
199
- gauthHost: this.url.GARMIN_SSO_EMBED,
200
- service: this.url.GARMIN_SSO_EMBED,
201
- source: this.url.GARMIN_SSO_EMBED,
202
- redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
203
- redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
204
- };
205
- const step3Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(signinParams)}`;
206
- // console.log('login - step3Url:', step3Url);
326
+ return csrfToken;
327
+ }
328
+ /**
329
+ * 执行登录步骤3:提交凭据
330
+ */
331
+ async performLoginStep3(username, password, csrfToken, step3Params) {
332
+ const step3Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(step3Params)}`;
333
+ // console.log('🚀 - getLoginTicket - step3Url:', step3Url);
207
334
  const step3Form = new form_data_1.default();
208
335
  step3Form.append('username', username);
209
336
  step3Form.append('password', password);
210
337
  step3Form.append('embed', 'true');
211
- step3Form.append('_csrf', csrf_token);
212
- const step3Result = await this.post(step3Url, step3Form, {
338
+ step3Form.append('_csrf', csrfToken);
339
+ return this.post(step3Url, step3Form, {
213
340
  headers: {
214
341
  'Content-Type': 'application/x-www-form-urlencoded',
215
342
  Dnt: 1,
216
343
  Origin: this.url.GARMIN_SSO_ORIGIN,
217
344
  Referer: this.url.SIGNIN_URL,
218
- 'User-Agent': USER_AGENT_BROWSER
345
+ 'User-Agent': USER_AGENT_CONNECTMOBILE
219
346
  }
220
347
  });
221
- // console.log('step3Result:', step3Result)
222
- this.handleAccountLocked(step3Result);
223
- this.handlePageTitle(step3Result);
224
- this.handleMFA(step3Result);
225
- const ticketRegResult = TICKET_RE.exec(step3Result);
226
- if (!ticketRegResult) {
227
- throw new Error('login failed (Ticket not found or MFA), please check username and password');
348
+ }
349
+ /**
350
+ * 判断是否需要MFA验证
351
+ */
352
+ isMFARequired(pageTitle) {
353
+ return pageTitle.toLowerCase().includes('mfa');
354
+ }
355
+ /**
356
+ * 从响应中提取票据
357
+ */
358
+ extractTicket(signinResult) {
359
+ const ticketRegResult = TICKET_RE.exec(signinResult);
360
+ return ticketRegResult ? ticketRegResult[1] : null;
361
+ }
362
+ /**
363
+ * 处理MFA验证(使用直接提供的验证码)
364
+ * @param htmlStr HTML响应字符串
365
+ * @param signinParams 登录参数
366
+ * @param mfaCode MFA验证码
367
+ * @returns MFA验证后的响应字符串
368
+ */
369
+ async handleMFAWithCode(htmlStr, signinParams, mfaCode) {
370
+ try {
371
+ // 提取CSRF令牌
372
+ const csrfToken = this.extractCsrfToken(htmlStr);
373
+ if (!csrfToken) {
374
+ throw new Error('MFA验证 - 未找到CSRF令牌');
375
+ }
376
+ // 提交MFA验证码
377
+ const mfaResult = await this.submitMFACode(csrfToken, mfaCode, signinParams);
378
+ // 验证MFA结果
379
+ return this.validateMFAResult(mfaResult);
228
380
  }
229
- const ticket = ticketRegResult[1];
230
- return ticket;
381
+ catch (error) {
382
+ console.error('MFA验证失败:', error);
383
+ throw new Error(`MFA验证失败: ${error}`);
384
+ }
385
+ }
386
+ /**
387
+ * 处理MFA验证
388
+ * @param htmlStr HTML响应字符串
389
+ * @param signinParams 登录参数
390
+ * @param mfaCallback MFA验证回调函数
391
+ * @returns MFA验证后的响应字符串
392
+ */
393
+ async handleMFA(htmlStr, signinParams, mfaCallback) {
394
+ // 验证MFA回调函数
395
+ if (!mfaCallback) {
396
+ throw new Error('登录失败(需要MFA验证),请提供MFA回调函数');
397
+ }
398
+ // 提取CSRF令牌
399
+ const csrfToken = this.extractCsrfToken(htmlStr);
400
+ // console.log('🚀 - handleMFA - csrfToken:', csrfToken);
401
+ if (!csrfToken) {
402
+ throw new Error('无法从MFA页面提取CSRF令牌');
403
+ }
404
+ // 获取MFA验证码
405
+ const mfaCode = await mfaCallback();
406
+ console.log('🚀 - handleMFA - mfaCode:', mfaCode);
407
+ // 提交MFA验证
408
+ const mfaResult = await this.submitMFACode(csrfToken, mfaCode, signinParams);
409
+ // 验证MFA结果
410
+ return this.validateMFAResult(mfaResult);
231
411
  }
232
- // TODO: Handle MFA
233
- handleMFA(htmlStr) { }
234
- // TODO: Handle Phone number
412
+ /**
413
+ * 提交MFA验证码
414
+ */
415
+ async submitMFACode(csrfToken, mfaCode, signinParams) {
416
+ const SSO = this.url.GARMIN_SSO;
417
+ const mfaForm = new form_data_1.default();
418
+ mfaForm.append('mfa-code', mfaCode);
419
+ mfaForm.append('embed', 'true');
420
+ mfaForm.append('_csrf', csrfToken);
421
+ return this.post(`${SSO}/verifyMFA/loginEnterMfaCode`, mfaForm, {
422
+ params: signinParams,
423
+ headers: {
424
+ 'Content-Type': 'application/x-www-form-urlencoded',
425
+ Dnt: 1,
426
+ Origin: this.url.GARMIN_SSO_ORIGIN,
427
+ Referer: `${SSO}/signin`,
428
+ 'User-Agent': USER_AGENT_BROWSER
429
+ },
430
+ maxRedirects: 10,
431
+ transformResponse: [
432
+ function (data, headers) {
433
+ if (headers.location &&
434
+ headers.location.includes('logintoken')) {
435
+ console.log('检测到重定向到包含logintoken的URL:', headers.location);
436
+ }
437
+ return data;
438
+ }
439
+ ]
440
+ });
441
+ }
442
+ /**
443
+ * 验证MFA结果
444
+ */
445
+ validateMFAResult(mfaResult) {
446
+ // console.log('MFA验证完成:', mfaResult);
447
+ const pageTitle = this.handlePageTitle(mfaResult);
448
+ console.log('MFA验证后的页面标题:', pageTitle);
449
+ return mfaResult;
450
+ }
451
+ /**
452
+ * 从HTML中提取CSRF令牌
453
+ * @param html HTML字符串
454
+ * @returns CSRF令牌或null
455
+ */
456
+ extractCsrfToken(html) {
457
+ const match = CSRF_RE.exec(html);
458
+ return match ? match[1] : null;
459
+ }
460
+ /**
461
+ * 处理页面标题
462
+ * @param htmlStr HTML字符串
463
+ * @returns 页面标题
464
+ */
235
465
  handlePageTitle(htmlStr) {
236
- const pageTitileRegResult = PAGE_TITLE_RE.exec(htmlStr);
237
- if (pageTitileRegResult) {
238
- const title = pageTitileRegResult[1];
239
- console.log('login page title:', title);
466
+ const pageTitleRegResult = PAGE_TITLE_RE.exec(htmlStr);
467
+ if (pageTitleRegResult) {
468
+ const title = pageTitleRegResult[1];
469
+ console.log('登录页面标题:', title);
240
470
  if (lodash_1.default.includes(title, 'Update Phone Number')) {
241
- // current I don't know where to update it
242
- // See: https://github.com/matin/garth/issues/19
243
- throw new Error('login failed (Update Phone number), please update your phone number, See: https://github.com/matin/garth/issues/19');
471
+ throw new Error('登录失败(需要更新电话号码),请更新您的电话号码,参考: https://github.com/matin/garth/issues/19');
244
472
  }
473
+ return title;
474
+ }
475
+ else {
476
+ throw new Error('登录失败(未找到页面标题)');
245
477
  }
246
478
  }
479
+ /**
480
+ * 处理账户锁定状态
481
+ * @param htmlStr HTML字符串
482
+ */
247
483
  handleAccountLocked(htmlStr) {
248
484
  const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr);
249
485
  if (accountLockedRegResult) {
250
486
  const msg = accountLockedRegResult[1];
251
487
  console.error(msg);
252
- throw new Error('login failed (AccountLocked), please open connect web page to unlock your account');
488
+ throw new Error('登录失败(账户已锁定),请打开Connect网页解锁您的账户');
253
489
  }
254
490
  }
491
+ /**
492
+ * 刷新OAuth2令牌
493
+ */
255
494
  async refreshOauth2Token() {
256
495
  try {
257
496
  if (!this.OAUTH_CONSUMER) {
258
497
  await this.fetchOauthConsumer();
259
498
  }
260
499
  if (!this.oauth2Token || !this.oauth1Token) {
261
- throw new Error('Missing required tokens for refresh');
500
+ throw new Error('缺少刷新令牌所需的必要令牌');
262
501
  }
263
502
  const oauth1 = {
264
503
  oauth: this.getOauthClient(this.OAUTH_CONSUMER),
265
504
  token: this.oauth1Token
266
505
  };
267
506
  await this.exchange(oauth1);
268
- console.log(`「${this.config.username}」in「${this.url.domain}OAuth2 token refreshed successfully`);
507
+ console.log(`「${this.config.username}」在「${this.url.domain}」的OAuth2令牌刷新成功`);
269
508
  }
270
509
  catch (error) {
271
- console.error('Failed to refresh OAuth2 token:', error);
510
+ console.error('刷新OAuth2令牌失败:', error);
272
511
  throw error;
273
512
  }
274
513
  }
514
+ /**
515
+ * 获取OAuth1令牌
516
+ * @param ticket 登录票据
517
+ * @returns OAuth1令牌和客户端
518
+ */
275
519
  async getOauth1Token(ticket) {
276
520
  if (!this.OAUTH_CONSUMER) {
277
- throw new Error('No OAUTH_CONSUMER');
521
+ throw new Error('未找到OAuth消费者信息');
278
522
  }
279
523
  const params = {
280
524
  ticket,
@@ -283,26 +527,28 @@ class HttpClient {
283
527
  };
284
528
  const url = `${this.url.OAUTH_URL}/preauthorized?${qs_1.default.stringify(params)}`;
285
529
  const oauth = this.getOauthClient(this.OAUTH_CONSUMER);
286
- const step4RequestData = {
530
+ const requestData = {
287
531
  url: url,
288
532
  method: 'GET'
289
533
  };
290
- const headers = oauth.toHeader(oauth.authorize(step4RequestData));
291
- // console.log('getOauth1Token - headers:', headers);
534
+ const headers = oauth.toHeader(oauth.authorize(requestData));
292
535
  const response = await this.get(url, {
293
536
  headers: {
294
537
  ...headers,
295
538
  'User-Agent': USER_AGENT_CONNECTMOBILE
296
539
  }
297
540
  });
298
- // console.log('getOauth1Token - response:', response);
299
541
  const token = qs_1.default.parse(response);
300
- // console.log('getOauth1Token - token:', token);
301
542
  this.oauth1Token = token;
302
543
  return { token, oauth };
303
544
  }
545
+ /**
546
+ * 获取OAuth客户端
547
+ * @param consumer OAuth消费者信息
548
+ * @returns OAuth客户端
549
+ */
304
550
  getOauthClient(consumer) {
305
- const oauth = new oauth_1_0a_1.default({
551
+ return new oauth_1_0a_1.default({
306
552
  consumer: consumer,
307
553
  signature_method: 'HMAC-SHA1',
308
554
  hash_function(base_string, key) {
@@ -312,25 +558,24 @@ class HttpClient {
312
558
  .digest('base64');
313
559
  }
314
560
  });
315
- return oauth;
316
561
  }
317
- //
562
+ /**
563
+ * 交换OAuth2令牌
564
+ * @param oauth1 OAuth1令牌和客户端
565
+ */
318
566
  async exchange(oauth1) {
319
567
  const token = {
320
568
  key: oauth1.token.oauth_token,
321
569
  secret: oauth1.token.oauth_token_secret
322
570
  };
323
- // console.log('exchange - token:', token);
324
571
  const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`;
325
572
  const requestData = {
326
573
  url: baseUrl,
327
574
  method: 'POST',
328
575
  data: null
329
576
  };
330
- const step5AuthData = oauth1.oauth.authorize(requestData, token);
331
- // console.log('login - step5AuthData:', step5AuthData);
332
- const url = `${baseUrl}?${qs_1.default.stringify(step5AuthData)}`;
333
- // console.log('exchange - url:', url);
577
+ const authData = oauth1.oauth.authorize(requestData, token);
578
+ const url = `${baseUrl}?${qs_1.default.stringify(authData)}`;
334
579
  this.oauth2Token = undefined;
335
580
  const response = await this.post(url, null, {
336
581
  headers: {
@@ -338,10 +583,13 @@ class HttpClient {
338
583
  'Content-Type': 'application/x-www-form-urlencoded'
339
584
  }
340
585
  });
341
- // console.log('exchange - response:', response);
342
586
  this.oauth2Token = this.setOauth2TokenExpiresAt(response);
343
- // console.log('exchange - oauth2Token:', this.oauth2Token);
344
587
  }
588
+ /**
589
+ * 设置OAuth2令牌过期时间
590
+ * @param token OAuth2令牌
591
+ * @returns 设置了过期时间的OAuth2令牌
592
+ */
345
593
  setOauth2TokenExpiresAt(token) {
346
594
  const now = luxon_1.DateTime.now();
347
595
  const expiresAt = now.plus({ seconds: token.expires_in });