-
Notifications
You must be signed in to change notification settings - Fork 85
/
Copy pathdjc_helper.py
9505 lines (7990 loc) · 396 KB
/
djc_helper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import datetime
import functools
import json
import os
import random
import re
import string
import time
import uuid
from multiprocessing import Pool
from typing import Any, Callable
from urllib import parse
from urllib.parse import parse_qsl, quote, quote_plus, unquote_plus, urlparse
import requests
import json_parser
from black_list import check_in_black_list
from config import AccountConfig, CommonConfig, ExchangeItemConfig, XinYueOperationConfig, config, load_config
from const import appVersion, cached_dir, sVersionName, vscode_online_url
from dao import (
XIN_YUE_MIN_LEVEL,
AmesvrQueryRole,
AmesvrUserBindInfo,
AmsActInfo,
BuyInfo,
ColgBattlePassInfo,
ColgBattlePassQueryInfo,
ComicDataList,
DnfChronicleMatchServerAddUserRequest,
DnfChronicleMatchServerCommonResponse,
DnfChronicleMatchServerRequestUserRequest,
DnfChronicleMatchServerRequestUserResponse,
DnfHelperChronicleBasicAwardInfo,
DnfHelperChronicleBasicAwardList,
DnfHelperChronicleBindInfo,
DnfHelperChronicleExchangeGiftInfo,
DnfHelperChronicleExchangeList,
DnfHelperChronicleLotteryList,
DnfHelperChronicleSignGiftInfo,
DnfHelperChronicleSignList,
DnfHelperChronicleUserActivityTopInfo,
DnfHelperChronicleUserTaskList,
DnfHelperEnergyTreeInfo,
DnfHelperEnergyTreeResponse,
DnfHelperLuckyLotteryInfo,
DnfHelperLuckyLotteryResponse,
DnfRoleInfo,
DnfRoleInfoList,
GameRoleInfo,
GoodsInfo,
IdeActInfo,
MaJieLuoInfo,
MobileGameGiftInfo,
NewArkLotteryAgreeRequestCardResult,
NewArkLotteryCardCountInfo,
NewArkLotteryLotteryCountInfo,
NewArkLotteryRequestCardResult,
NewArkLotterySendCardResult,
RoleInfo,
ShenJieGrowUpCurStageData,
ShenJieGrowUpInfo,
ShenJieGrowUpStagePack,
ShenJieGrowUpTaskData,
TemporaryChangeBindRoleInfo,
XiaojiangyouInfo,
XiaojiangyouPackageInfo,
XinYueBattleGroundWpeBindRole,
XinYueBattleGroundWpeGetBindRoleResult,
XinYueBgwUserInfo,
XinYueInfo,
XinYueMatchServerAddTeamRequest,
XinYueMatchServerCommonResponse,
XinYueMatchServerRequestTeamRequest,
XinYueMatchServerRequestTeamResponse,
XinYueMyTeamInfo,
XinYueSummaryTeamInfo,
XinYueTeamAwardInfo,
XinYueTeamGroupInfo,
parse_amesvr_common_info,
)
from data_struct import to_raw_type
from db import CacheDB, CacheInfo, DnfHelperChronicleExchangeListDB, DnfHelperChronicleUserActivityTopInfoDB, WelfareDB
from encrypt import make_dnf_helper_signature, make_dnf_helper_signature_data
from exceptions_def import (
ArkLotteryTargetQQSendByRequestReachMaxCount,
DnfHelperChronicleTokenExpiredOrWrongException,
GithubActionLoginException,
SameAccountTryLoginAtMultipleThreadsException,
)
from first_run import is_daily_first_run, is_first_run, is_monthly_first_run, is_weekly_first_run, reset_first_run
from game_info import get_game_info, get_game_info_by_bizcode
from log import color, logger
from network import Network, check_tencent_game_common_status_code, jsonp_callback_flag
from qq_login import LoginResult, QQLogin
from server import get_match_server_api
from setting import dnf_server_id_to_area_info, dnf_server_id_to_name
from sign import getACSRFTokenForAMS, getMillSecondsUnix
from urls import (
Urls,
get_act_url,
get_ams_act,
get_ams_act_desc,
get_ide_act,
get_ide_act_desc,
get_not_ams_act,
get_not_ams_act_desc,
not_know_end_time____,
)
from usage_count import increase_counter
from util import (
async_message_box,
base64_decode,
base64_encode,
double_quote,
double_unquote,
extract_between,
filter_unused_params_catch_exception,
format_now,
format_time,
get_first_exists_dict_value,
get_logger_func,
get_meaningful_call_point_for_log,
get_month,
get_now,
get_this_thursday_of_dnf,
get_this_week_monday_datetime,
get_today,
get_week,
is_act_expired,
json_compact,
message_box,
now_after,
now_before,
now_in_range,
padLeftRight,
parse_time,
pause,
pause_and_exit,
post_json_to_data,
range_from_one,
remove_suffix,
show_act_not_enable_warning,
show_head_line,
show_quick_edit_mode_tip,
start_and_end_date_of_a_month,
tableify,
triple_quote,
try_except,
uin2qq,
urlsafe_base64_decode,
urlsafe_base64_encode,
use_by_myself,
utf8len,
wait_for,
will_act_expired_in,
with_cache,
with_retry,
)
# DNF蚊子腿小助手
class DjcHelper:
local_saved_skey_file = os.path.join(cached_dir, ".saved_skey.{}.json")
local_saved_pskey_file = os.path.join(cached_dir, ".saved_pskey.{}.json")
local_saved_teamid_file = os.path.join(cached_dir, ".teamid_v2.{}.json")
def __init__(self, account_config, common_config, user_buy_info: BuyInfo | None = None):
self.cfg: AccountConfig = account_config
self.common_cfg: CommonConfig = common_config
# 初始化部分字段
self.lr: LoginResult | None = None
# 配置加载后,尝试读取本地缓存的skey
self.local_load_uin_skey()
# 初始化网络相关设置
self.init_network()
# 相关链接
self.urls = Urls()
self.user_buy_info = user_buy_info
# --------------------------------------------一些辅助函数--------------------------------------------
def init_network(self):
self.network = Network(self.cfg.sDeviceID, self.uin(), self.cfg.account_info.skey, self.common_cfg)
def check_skey_expired(self, window_index=1):
# note: 使用一些长期有效的活动(如旧版心悦战场、福利中心)的接口来判断skey是否过期
# 未过期:
# {"flowRet": {...}, "modRet": {...}, "ret": "0", "msg": ""}
# {"ret": "-1", "msg": "目前访问人数过多!请稍后再试!谢谢!", "flowRet": {...}}
# 已过期:
# {"ret": "101", "msg": "非常抱歉,请先登录!", "flowRet": ...}
# re:
# 活动本身结束或维护中,无法判断skey是否过期
# {'ret': '300', 'msg': '抱歉,系统升级维护中', "flowRet": ...}
# {"ret": "301", "msg": "非常抱歉,该活动已经结束!", "flowRet": ...}
# {'iRet': 302, 'ret': 302, 'sAmsSerial': 'AMS-DNF-0303222833-z9dpS5-603984-248455', 'sMsg': '抱歉,当前活动已结束!'}
# res = self.xinyue_battle_ground_op("判断skey是否过期", "767160", print_res=False)
# res = self.dnf_welfare_op("判断skey是否过期", "649261", print_res=False)
# res = self.dnf_comic_ide_op("判断skey是否过期", "248455", print_res=False)
res = self.dnf_bind_phone_op("判断skey是否过期", "971619", print_res=False)
if use_by_myself():
msg = str(get_first_exists_dict_value(res, "msg", "sMsg") or "")
if str(res.get("ret", "")) in ["300", "301", "302"] or "结束" in msg:
async_message_box(
"用于判断skey是否过期的活动本身已经结束,需要看下是否需要换个新活动来判断",
"(仅自己可见)skey活动结束",
)
if str(res["ret"]) != "101":
# skey尚未过期,则重新刷一遍,主要用于从qq空间获取的情况
account_info = self.cfg.account_info
self.save_uin_skey(account_info.uin, account_info.skey, self.get_vuserid())
else:
# 已过期,更新skey
logger.info("")
logger.warning(f"账号({self.cfg.name})的skey已过期,即将尝试更新skey")
self.update_skey(window_index=window_index)
# skey获取完毕后,检查是否在黑名单内
check_in_black_list(self.uin())
def update_skey(self, window_index=1):
if self.cfg.function_switches.disable_login_mode_normal:
logger.warning("禁用了普通登录模式,将不会尝试更新skey")
return
login_mode_dict: dict[str, Callable[[int], None]] = {
"by_hand": self.update_skey_by_hand,
"qr_login": self.update_skey_qr_login,
"auto_login": self.update_skey_auto_login,
}
login_mode_dict[self.cfg.login_mode](window_index)
def update_skey_by_hand(self, window_index=1):
js_code = """cookies=Object.fromEntries(document.cookie.split(/; */).map(cookie => cookie.split('=', 2)));console.log("uin="+cookies.uin+"\\nskey="+cookies.skey+"\\n");"""
fallback_js_code = """document.cookie.split(/; */);"""
logger.error(
"skey过期,请按下列步骤获取最新skey并更新到配置中\n"
"1. 在本脚本自动打开的活动网页中使用通用登录组件完成登录操作\n"
" 1.1 指点击(亲爱的玩家,请【登录】)中的登录按钮,并完成后续登录操作\n"
"2. 点击F12,将默认打开DevTools(开发者工具界面)的Console界面\n"
" 如果默认不是该界面,则点击上方第二个tab(Console)(中文版这个tab的名称可能是命令行?)\n"
"3. 在下方输入区输入下列内容来从cookie中获取uin和skey(或者直接粘贴,默认已复制到系统剪贴板里了)\n"
f" {js_code}\n"
"-- 如果上述代码执行报错,可能是因为浏览器不支持,这时候可以复制下面的代码进行上述操作\n"
" 执行后,应该会显示一个可点开的内容,戳一下会显示各个cookie的内容,然后手动在里面查找uin和skey即可\n"
f" {fallback_js_code}\n"
"3. 将uin/skey的值分别填写到config.toml中对应配置的值中即可\n"
"4. 填写dnf的区服和手游的区服信息到config.toml中\n"
"5. 正常使用还需要填写完成后再次运行脚本,获得角色相关信息,并将信息填入到config.toml中\n"
"\n"
f"具体信息为可见前面的日志"
)
# 打开配置界面
cfgFile = "./config.toml"
localCfgFile = "./config.toml.local"
if os.path.isfile(localCfgFile):
cfgFile = localCfgFile
async_message_box(
f"请使用网页版vscode或者下载个本地版的vscode打开【{cfgFile}】文件 第53行 来自行修改~",
"提示",
open_url=vscode_online_url,
)
# # 复制js代码到剪贴板,方便复制
# pyperclip.copy(js_code)
# 打开活动界面
os.popen("start https://dnf.qq.com/lbact/a20200716wgmhz/index.html?wg_ad_from=loginfloatad")
# 提示
input("\n完成上述操作后点击回车键即可退出程序,重新运行即可...")
pause_and_exit(-1)
def update_skey_qr_login(self, window_index=1):
qqLogin = QQLogin(self.common_cfg, window_index=window_index)
loginResult = qqLogin.qr_login(
QQLogin.login_mode_normal, name=self.cfg.name, account=self.cfg.account_info.account
)
self.save_uin_skey(loginResult.uin, loginResult.skey, loginResult.vuserid)
def update_skey_auto_login(self, window_index=1):
qqLogin = QQLogin(self.common_cfg, window_index=window_index)
ai = self.cfg.account_info
loginResult = qqLogin.login(ai.account, ai.password, QQLogin.login_mode_normal, name=self.cfg.name)
self.save_uin_skey(loginResult.uin, loginResult.skey, loginResult.vuserid)
def save_uin_skey(self, uin, skey, vuserid):
self.memory_save_uin_skey(uin, skey)
self.local_save_uin_skey(uin, skey, vuserid)
def local_save_uin_skey(self, uin, skey, vuserid):
# 本地缓存
self.set_vuserid(vuserid)
with open(self.get_local_saved_skey_file(), "w", encoding="utf-8") as sf:
loginResult = {
"uin": str(uin),
"skey": str(skey),
"vuserid": str(vuserid),
}
json.dump(loginResult, sf)
logger.debug(f"本地保存skey信息,具体内容如下:{loginResult}")
logger.debug("同时由于在pskey的缓存中也有一份skey数据, 去读取过来更新这部分字段,确保两边最终一致")
cached_pskey = self.load_uin_pskey()
if cached_pskey is not None:
self.save_uin_pskey(
cached_pskey["p_uin"],
cached_pskey["p_skey"],
skey,
vuserid,
)
def local_load_uin_skey(self):
# 仅二维码登录和自动登录模式需要尝试在本地获取缓存的信息
if self.cfg.login_mode not in ["qr_login", "auto_login"]:
return
# 若未有缓存文件,则跳过
if not os.path.isfile(self.get_local_saved_skey_file()):
return
with open(self.get_local_saved_skey_file(), encoding="utf-8") as f:
try:
loginResult = json.load(f)
except json.decoder.JSONDecodeError:
logger.error(f"账号 {self.cfg.name} 的skey缓存已损坏,将视为已过期")
return
self.memory_save_uin_skey(loginResult["uin"], loginResult["skey"])
self.set_vuserid(loginResult.get("vuserid", ""))
logger.debug(f"读取本地缓存的skey信息,具体内容如下:{loginResult}")
def get_local_saved_skey_file(self):
return self.local_saved_skey_file.format(self.cfg.name)
def memory_save_uin_skey(self, uin, skey):
# 保存到内存中
self.cfg.updateUinSkey(uin, skey)
# uin, skey更新后重新初始化网络相关
self.init_network()
def set_vuserid(self, vuserid: str):
self.vuserid = vuserid
def get_vuserid(self) -> str:
return getattr(self, "vuserid", "")
# --------------------------------------------获取角色信息和游戏信息--------------------------------------------
@with_retry(max_retry_count=3)
def get_bind_role_list(self, print_warning=True):
self.fetch_djc_login_info("获取绑定角色列表", print_warning)
# 查询全部绑定角色信息
res = self.get(
"获取道聚城各游戏的绑定角色列表",
self.urls.query_bind_role_list,
print_res=False,
use_this_cookies=self.djc_custom_cookies,
)
roleinfo_list = res.get("data", [])
db = CacheInfo()
db.with_context(f"绑定角色缓存/{self.cfg.get_account_cache_key()}").load()
if len(roleinfo_list) != 0:
# 成功请求时,保存一份数据到本地
db.value = roleinfo_list
db.save()
else:
get_logger_func(print_warning)("获取绑定角色失败了,尝试使用本地缓存的角色信息")
logger.debug(f"缓存的信息为 {db.value}")
roleinfo_list = db.value or []
self.bizcode_2_bind_role_map = {}
for roleinfo_dict in roleinfo_list:
role_info = GameRoleInfo().auto_update_config(roleinfo_dict)
self.bizcode_2_bind_role_map[role_info.sBizCode] = role_info
def get_dnf_bind_role_copy(self) -> RoleInfo:
return self.get_dnf_bind_role().clone()
def get_dnf_bind_role(self) -> RoleInfo | None:
roleinfo = None
if self.cfg.bind_role.has_config():
# 如果配置了绑定角色,则优先使用这个
server_id, role_id = self.cfg.bind_role.dnf_server_id, self.cfg.bind_role.dnf_role_id
role_info_from_web = self.query_dnf_role_info_by_serverid_and_roleid(server_id, role_id)
server_name = dnf_server_id_to_name(server_id)
area_info = dnf_server_id_to_area_info(server_id)
# 构造一份道聚城绑定角色信息,简化改动
djc_role_info = RoleInfo()
djc_role_info.roleCode = role_id
djc_role_info.roleName = role_info_from_web.rolename
djc_role_info.serviceID = server_id
djc_role_info.serviceName = server_name
djc_role_info.areaID = area_info.v
djc_role_info.areaName = area_info.t
logger.debug("使用本地配置的角色来领奖")
roleinfo = djc_role_info
else:
# 否则尝试使用道聚城绑定的角色信息
game_name = "dnf"
if game_name in self.bizcode_2_bind_role_map:
roleinfo = self.bizcode_2_bind_role_map[game_name].sRoleInfo
return roleinfo
def get_fz_bind_role(self) -> RoleInfo | None:
"""
获取命运方舟的绑定角色信息
"""
roleinfo = None
game_name = "fz"
if game_name in self.bizcode_2_bind_role_map:
roleinfo = self.bizcode_2_bind_role_map[game_name].sRoleInfo
return roleinfo
def get_mobile_game_info(self):
# 如果游戏名称设置为【任意手游】,则从绑定的手游中随便挑一个
if self.cfg.mobile_game_role_info.use_any_binded_mobile_game():
found_binded_game = False
for _bizcode, bind_role_info in self.bizcode_2_bind_role_map.items():
if bind_role_info.is_mobile_game():
self.cfg.mobile_game_role_info.game_name = bind_role_info.sRoleInfo.gameName
found_binded_game = True
logger.warning(
f"当前手游名称配置为任意手游,将从道聚城已绑定的手游中随便选一个,挑选为:{self.cfg.mobile_game_role_info.game_name}"
)
break
if not found_binded_game:
return None
return get_game_info(self.cfg.mobile_game_role_info.game_name)
# --------------------------------------------各种操作--------------------------------------------
def run(self, user_buy_info: BuyInfo):
self.normal_run(user_buy_info)
# 预处理阶段
def check_djc_role_binding(self) -> bool:
# 指引获取uin/skey/角色信息等
self.check_skey_expired()
# 尝试获取绑定的角色信息
self.get_bind_role_list()
# 检查绑定信息
binded = True
if self.cfg.function_switches.get_djc:
# 检查道聚城是否已绑定dnf角色信息,若未绑定则警告(这里不停止运行是因为可以不配置领取dnf的道具)
if not self.cfg.cannot_bind_dnf_v2 and "dnf" not in self.bizcode_2_bind_role_map:
logger.warning(
color("fg_bold_yellow") + "未在道聚城绑定【地下城与勇士】的角色信息,请前往道聚城app进行绑定"
)
binded = False
# if self.cfg.mobile_game_role_info.enabled() and not self.check_mobile_game_bind():
# logger.warning(color("fg_bold_green") + "!!!请注意,我说的是手游,不是DNF!!!")
# logger.warning(color("fg_bold_green") + "如果不需要做道聚城的手游任务和许愿任务(不做会少豆子),可以在配置工具里将手游名称设为无")
# binded = False
if binded:
if self.cfg.function_switches.get_djc:
# 打印dnf和手游的绑定角色信息
logger.info("已获取道聚城目前绑定的角色信息如下")
games = []
if "dnf" in self.bizcode_2_bind_role_map:
games.append("dnf")
# if self.cfg.mobile_game_role_info.enabled():
# games.append(self.get_mobile_game_info().bizCode)
for bizcode in games:
roleinfo = self.bizcode_2_bind_role_map[bizcode].sRoleInfo
logger.info(
f"{roleinfo.gameName}: ({roleinfo.serviceName}-{roleinfo.roleName}-{roleinfo.roleCode})"
)
else:
logger.warning("当前账号未启用道聚城相关功能")
if self.cfg.bind_role.has_config():
# 若本地配置了领奖角色,则强制认为已绑定
binded = True
return binded
def check_mobile_game_bind(self):
# 检查配置的手游是否有效
gameinfo = self.get_mobile_game_info()
if gameinfo is None:
logger.warning(
color("fg_bold_yellow")
+ "当前手游名称配置为【任意手游】,但未在道聚城找到任何绑定的手游,请前往道聚城绑定任意一个手游,如王者荣耀"
)
return False
# 检查道聚城是否已绑定该手游的角色,若未绑定则警告并停止运行
bizcode = gameinfo.bizCode
if bizcode not in self.bizcode_2_bind_role_map:
logger.warning(
color("fg_bold_yellow")
+ f"未在道聚城绑定手游【{get_game_info_by_bizcode(bizcode).bizName}】的角色信息,请前往道聚城app进行绑定。"
)
logger.warning(
color("fg_bold_cyan")
+ "若想绑定其他手游则调整【配置工具】配置的手游名称,"
+ color("fg_bold_blue")
+ "若不启用则将手游名称调整为无"
)
return False
# 检查这个游戏是否是手游
role_info = self.bizcode_2_bind_role_map[bizcode]
if not role_info.is_mobile_game():
logger.warning(
color("fg_bold_yellow") + f"【{get_game_info_by_bizcode(bizcode).bizName}】是端游,不是手游。"
)
logger.warning(
color("fg_bold_cyan")
+ "若想绑定其他手游则调整【配置工具】配置的手游名称,"
+ color("fg_bold_blue")
+ "若不启用则将手游名称调整为无"
)
return False
return True
# 正式运行阶段
def normal_run(self, user_buy_info: BuyInfo):
# 检查skey是否过期
self.check_skey_expired()
# 获取dnf和手游的绑定信息
self.get_bind_role_list()
# 运行活动
activity_funcs_to_run = self.get_activity_funcs_to_run(user_buy_info)
for _act_name, activity_func in activity_funcs_to_run:
activity_func()
# # 以下为并行执行各个活动的调用方式
# # 由于下列原因,该方式基本确定不会再使用
# # 1. amesvr活动服务器会限制调用频率,如果短时间内请求过快,会返回401,并提示请求过快
# # 而多进程处理活动的时候,会非常频繁的触发这种情况,感觉收益不大。另外频繁触发这个警报,感觉也有可能会被腾讯风控,到时候就得不偿失了
# # 2. python的multiprocessing.pool.Pool不支持在子进程中再创建新的子进程
# # 因此在不同账号已经在不同的进程下运行的前提下,子进程下不能再创建新的子进程了
# async_run_all_act(self.cfg, self.common_cfg, activity_funcs_to_run)
def get_activity_funcs_to_run(self, user_buy_info: BuyInfo) -> list[tuple[str, Callable]]:
activity_funcs_to_run = []
activity_funcs_to_run.extend(self.free_activities())
if user_buy_info.is_active():
# 付费期间将付费活动也加入到执行列表中
activity_funcs_to_run.extend(self.payed_activities())
return activity_funcs_to_run
@try_except(show_exception_info=False)
def show_activities_summary(self, user_buy_info: BuyInfo):
# 需要运行的活动
free_activities = self.free_activities()
paied_activities = self.payed_activities()
# 展示活动的信息
def get_activities_summary(categray: str, activities: list) -> str:
activities_summary = ""
if len(activities) != 0:
activities_summary += f"\n目前的{categray}活动如下:"
heads, colSizes = zip(
("序号", 4),
("活动名称", 24),
("结束于", 12),
("剩余天数", 8),
("活动链接为", 50),
)
activities_summary += "\n" + color("bold_green") + tableify(heads, colSizes)
for idx, name_and_func in enumerate(activities):
act_name, act_func = name_and_func
op_func_name = act_func.__name__ + "_op"
end_time = parse_time(not_know_end_time____)
# 可能是非ams活动
act_info = None
try:
act_info = get_not_ams_act(act_name)
if act_info is None and hasattr(self, op_func_name):
# 可能是ams或ide活动
act_info = getattr(self, op_func_name)("获取活动信息", "", get_act_info_only=True)
except Exception as e:
logger.debug(f"请求{act_name} 出错了", exc_info=e)
if act_info is not None:
end_time = parse_time(act_info.get_endtime())
line_color = "bold_green"
if is_act_expired(format_time(end_time)):
line_color = "bold_black"
end_time_str = format_time(end_time, "%Y-%m-%d")
remaining_days = (end_time - get_now()).days
print_act_name = padLeftRight(act_name, colSizes[1], mode="left", need_truncate=True)
act_url = padLeftRight(get_act_url(act_name), colSizes[-1], mode="left")
# activities_summary += with_color(line_color, f'\n {idx + 1:2d}. {print_act_name} 将结束于{end_time_str}(剩余 {remaining_days:3d} 天),活动链接为: {act_url}')
activities_summary += (
"\n"
+ color(line_color)
+ tableify(
[idx + 1, print_act_name, end_time_str, remaining_days, act_url],
colSizes,
need_truncate=False,
)
)
else:
activities_summary += f"\n目前尚无{categray}活动,当新的{categray}活动出现时会及时加入~"
return activities_summary
# 提示如何复制
if self.common_cfg.disable_cmd_quick_edit:
show_quick_edit_mode_tip()
# 免费活动信息
free_activities_summary = get_activities_summary("长期免费", free_activities)
show_head_line("以下为免费的长期活动", msg_color=color("bold_cyan"))
logger.info(free_activities_summary)
# 付费活动信息
paied_activities_summary = get_activities_summary("短期付费", paied_activities)
show_head_line("以下为付费期间才会运行的短期活动", msg_color=color("bold_cyan"))
if not user_buy_info.is_active():
if user_buy_info.total_buy_month != 0:
msg = f"账号{user_buy_info.qq}的付费内容已到期,到期时间点为{user_buy_info.expire_at}。"
else:
msg = f"账号{user_buy_info.qq}未购买付费内容。"
msg += "\n因此2021-02-06之后添加的短期新活动将被跳过,如果想要启用该部分内容,可查看目录中的【付费指引/付费指引.docx】,目前定价为5元每月。"
msg += "\n2021-02-06之前添加的所有活动不受影响,仍可继续使用。"
msg += "\n具体受影响的活动内容如下"
logger.warning(color("bold_yellow") + msg)
logger.info(paied_activities_summary)
def free_activities(self) -> list[tuple[str, Callable]]:
return [
("道聚城", self.djc_operations),
("DNF地下城与勇士心悦特权专区", self.xinyue_battle_ground),
("心悦app", self.xinyue_app_operations),
("dnf论坛签到", self.dnf_bbs),
("小酱油周礼包和生日礼包", self.xiaojiangyou),
]
def payed_activities(self) -> list[tuple[str, Callable]]:
# re: 更新新的活动时记得更新urls.py的 not_ams_activities
# ? NOTE: 同时顺带更新 配置工具功能开关列表 act_category_to_act_desc_switch_list
# undone: 常用过滤词
# -aegis -beacon -log?sCloudApiName -.png -.jpg -.gif -.js -.css -.ico -data:image -.mp4 -pingfore.qq.com -.mp3 -.wav -logs.game.qq.com -fx_fe_report -trace.qq.com -.woff2 -.TTF -.otf -snowflake.qq.com -vd6.l.qq.com -doGPMReport -wuji/object -thumbplayer -get_video_mark_all -rumt-zh.com -login/analysis
return [
("DNF助手编年史", self.dnf_helper_chronicle),
("绑定手机活动", self.dnf_bind_phone),
("助手魔界人每日幸运签", self.dnf_helper_lucky_lottery),
("colg每日签到", self.colg_signin),
("超级会员", self.dnf_super_vip),
("DNF落地页活动_ide", self.dnf_luodiye_ide),
("回流引导秘籍", self.dnf_recall_guide),
("共赴西装节", self.dnf_suit),
]
def expired_activities(self) -> list[tuple[str, Callable]]:
# re: 记得过期活动全部添加完后,一个个确认下确实过期了
# hack: 已经过期非常久且很久未再出的的活动相关信息已挪到 djc_helper_tomb.py ,需要时可前往查看
# undone: 当这个列表下方过期很久的活动变得很多的时候,就再将部分挪到上面这个墓地中
return [
("助手能量之芽", self.dnf_helper_energy_tree),
("DNF福利中心兑换", self.dnf_welfare),
("DNF预约", self.dnf_reservation),
("DNF漫画预约活动", self.dnf_comic),
("新春充电计划", self.new_year_signin),
("超核勇士wpe", self.dnf_chaohe_wpe),
("WeGame活动", self.dnf_wegame),
("DNF心悦wpe", self.dnf_xinyue_wpe),
("集卡", self.dnf_ark_lottery),
("colg其他活动", self.colg_other_act),
("喂养删除补偿", self.weiyang_compensate),
("嘉年华星与心愿", self.dnf_star_and_wish),
("回流攻坚队", self.dnf_socialize),
("DNF神界成长之路", self.dnf_shenjie_grow_up),
("DNF神界成长之路二期", self.dnf_shenjie_grow_up_v2),
("DNF神界成长之路三期", self.dnf_shenjie_grow_up_v3),
("DNF卡妮娜的心愿摇奖机", self.dnf_kanina),
("勇士的冒险补给", self.maoxian),
("DNF格斗大赛", self.dnf_pk),
("DNF周年庆登录活动", self.dnf_anniversary),
("DNF落地页活动_ide_dup", self.dnf_luodiye_ide_dup),
("DNFxSNK", self.dnf_snk),
("DNF年货铺", self.dnf_nianhuopu),
("dnf助手活动wpe", self.dnf_helper_wpe),
("拯救赛利亚", self.dnf_save_sailiyam),
("DNF马杰洛的规划", self.majieluo),
("神界预热", self.dnf_shenjie_yure),
("qq视频蚊子腿-爱玩", self.qq_video_iwan),
("DNF落地页活动", self.dnf_luodiye),
("DNF娱乐赛", self.dnf_game),
("dnf助手活动", self.dnf_helper),
]
# --------------------------------------------道聚城--------------------------------------------
@try_except()
def djc_operations(self):
show_head_line("开始道聚城相关操作")
self.show_not_ams_act_info("道聚城")
if not self.cfg.function_switches.get_djc:
show_act_not_enable_warning("道聚城")
return
self.fetch_djc_login_info("获取道聚城登录信息")
# ------------------------------初始工作------------------------------
old_allin, old_balance = self.query_balance("1. 操作前:查询余额")
# self.query_money_flow("1.1 操作前:查一遍流水")
# ------------------------------核心逻辑------------------------------
# 自动签到
self.sign_in_and_take_awards()
# 完成任务
self.complete_tasks()
# 领取奖励并兑换道具
self.take_task_awards_and_exchange_items()
# ------------------------------清理工作------------------------------
new_allin, new_balance = self.query_balance("5. 操作全部完成后:查询余额")
# self.query_money_flow("5.1 操作全部完成后:查一遍流水")
delta = new_allin - old_allin
logger.warning(
color("fg_bold_yellow")
+ f"账号 {self.cfg.name} 本次道聚城操作共获得 {delta} 个豆子(历史总获取: {old_allin} -> {new_allin} 余额: {old_balance} -> {new_balance} )"
)
@try_except(return_val_on_except=(0, 0))
def query_balance(self, ctx, print_res=True) -> tuple[int, int]:
res = self.raw_query_balance(ctx, print_res)
if int(res["ret"]) != 0:
logger.warning(color("bold_cyan") + f"查询聚豆余额异常,返回结果为 {res}")
return 0, 0
info = res["data"]
allin, balance = int(info["allin"]), int(info["balance"])
return allin, balance
def raw_query_balance(self, ctx, print_res=True):
return self.get(ctx, self.urls.balance, print_res=print_res, use_this_cookies=self.djc_custom_cookies)
def query_money_flow(self, ctx):
return self.get(ctx, self.urls.money_flow)
# urls.sign签到接口偶尔会报 401 Unauthorized,因此需要加一层保护,确保不影响其他流程
@try_except()
def sign_in_and_take_awards(self):
self.get("2.1.2 发送app登录事件", self.urls.user_login_event, use_this_cookies=self.djc_custom_cookies)
total_try = self.common_cfg.retry.max_retry_count
for try_idx in range_from_one(total_try):
try:
# 签到
# note: 如果提示下面这行这样的日志,则尝试下载最新apk包,解包后确认下 aes_key 与 djc_rsa_public_key_new.der 是否有变动,若有变动,则替换为新的
# 目前访问人数过多!请稍后再试!谢谢!
# note:
# aes_key: 用 jadx-gui 反编译apk包后,搜索 sDjcSign,在这行里那个字符串就是
# djc_rsa_public_key_new.der: 解压apk包后,在 assets 目录里可以找到这个,用 HxD 等二进制对比工具看看是否与 utils/reference_data/public_key.der 的目录一直
#
# 签到流程@see assets/homepage_recommend_follow.js 搜索 自动签到的workflow
self.post("2.2 签到", self.urls.sign, self.sign_flow_data("96939"))
self.post("2.3 领取签到赠送的聚豆", self.urls.sign, self.sign_flow_data("324410"))
# 尝试领取自动签到的奖励
# 查询本月签到的日期列表
res = self.post("查询签到", self.urls.sign, self.sign_flow_data("321961"), print_res=False)
month_total_signed_days = int(res["modRet"]["jData"]["monthNum"])
logger.info(f"本月签到次数为 {month_total_signed_days}")
# 根据本月已签到数,领取符合条件的每月签到若干日的奖励(也就是聚豆页面最上面的那个横条)
sign_reward_rule_list = [
("累计3天领取5聚豆", "322021"),
("累计7天领取15聚豆", "322036"),
("累计10天领取20聚豆", "322037"),
("累计15天领取25聚豆", "322038"),
("累计20天领取30聚豆", "322039"),
("累计25天领取50聚豆", "322040"),
("累计签到整月-全勤奖", "881740"),
]
for ctx, iFlowId in sign_reward_rule_list:
res = self.post(ctx, self.urls.sign, self.sign_flow_data(iFlowId))
# 你的签到次数不够,请继续努力哦~
if "签到次数不够" in res["flowRet"]["sMsg"]:
break
break
except json.decoder.JSONDecodeError as e:
logger.error(f"第 {try_idx}/{total_try} 次尝试道聚城签到相关操作失败了,等待一会重试", exc_info=e)
if try_idx != total_try:
wait_for("道聚城签到操作失败", self.common_cfg.retry.retry_wait_time)
def sign_flow_data(self, iFlowId):
return self.format(self.urls.sign_raw_data, iFlowId=iFlowId)
def djc_operations_op(self, ctx, iFlowId, print_res=True, **extra_params):
iActivityId = self.urls.iActivityId_djc_operations
return self.amesvr_request(
ctx,
"comm.ams.game.qq.com",
"djc",
"dj",
iActivityId,
iFlowId,
print_res,
"",
**extra_params,
)
def complete_tasks(self):
# 完成《打卡活动中心》 @see assets\activity_home.js 搜索 activity_center
self.get(
"3.1 模拟点开活动中心",
self.urls.task_report,
task_type="activity_center",
use_this_cookies=self.djc_custom_cookies,
)
if self.cfg.mobile_game_role_info.enabled():
# 完成《礼包达人》
self.take_mobile_game_gift()
else:
async_message_box(
f"账号 {self.cfg.name} 未启用自动完成《礼包达人》任务功能,如需启用,请配置道聚城的手游名称。不配置,则每日任务的豆子会领不全",
"道聚城参数未配置",
show_once=True,
)
if self.cfg.function_switches.make_wish:
# todo: 完成《有理想》
self.make_wish()
else:
async_message_box(
f"账号 {self.cfg.name} 未启用自动完成《有理想》任务功能,如需启用,请打开道聚城许愿功能。不配置,则每日任务的豆子会领不全",
"道聚城参数未配置",
show_once=True,
)
# todo: 浏览3个活动
@try_except()
def take_mobile_game_gift(self):
game_info = self.get_mobile_game_info()
role_info = self.bizcode_2_bind_role_map[game_info.bizCode].sRoleInfo
giftInfos = self.get_mobile_game_gifts()
if len(giftInfos) == 0:
logger.warning(f"未找到手游【{game_info.bizName}】的有效七日签到配置,请换个手游,比如王者荣耀")
return
dayIndex = datetime.datetime.now().weekday() # 0-周一...6-周日,恰好跟下标对应
giftInfo = giftInfos[dayIndex]
# 很抱歉,您的请求签名校验未通过,请稍后再试哦!
# fixme: 这个会提示签名不对,但是看了代码,暂时没发现是为啥,先不管了,以后能弄懂再解决这个
self.get(
f"3.2 一键领取{role_info.gameName}日常礼包-{giftInfo.sTask}",
self.urls.receive_game_gift,
bizcode=game_info.bizCode,
iruleId=giftInfo.iRuleId,
systemID=role_info.systemID,
sPartition=role_info.areaID,
channelID=role_info.channelID,
channelKey=role_info.channelKey,
roleCode=role_info.roleCode,
sRoleName=quote_plus(role_info.roleName),
use_this_cookies=self.djc_custom_cookies,
)
@try_except()
def make_wish(self):
bizCode = "yxzj"
if bizCode not in self.bizcode_2_bind_role_map:
logger.warning(
color("fg_bold_cyan")
+ "未在道聚城绑定王者荣耀,将跳过许愿功能。建议使用安卓模拟器下载道聚城,在上面绑定王者荣耀"
)
return
roleModel = self.bizcode_2_bind_role_map[bizCode].sRoleInfo
if "苹果" in roleModel.channelKey:
logger.warning(
color("fg_bold_cyan")
+ f"ios端不能许愿手游,建议使用安卓模拟器下载道聚城,在上面绑定王者荣耀。roleModel={roleModel}"
)
return
# 查询许愿道具信息
query_wish_item_list_res = self.get(
"3.3.0 查询许愿道具",
self.urls.query_wish_goods_list,
plat=roleModel.systemID,
biz=roleModel.bizCode,
print_res=False,
)
if "data" not in query_wish_item_list_res or len(query_wish_item_list_res["data"]) == 0:
logger.warning(
f"在{roleModel.systemKey}上游戏【{roleModel.gameName}】暂不支持许愿,query_wish_item_list_res={query_wish_item_list_res}"
)
return
# 查询许愿列表
wish_list_res = self.get(
"3.3.1 查询许愿列表",
self.urls.query_wish,
appUid=self.qq(),
use_this_cookies=self.djc_custom_cookies,
print_res=False,
)
# 删除已经许愿的列表,确保许愿成功
for wish_info in wish_list_res["data"]["list"]:
ctx = f"3.3.2 删除已有许愿-{wish_info['bizName']}-{wish_info['sGoodsName']}"
self.get(ctx, self.urls.delete_wish, sKeyId=wish_info["sKeyId"], use_this_cookies=self.djc_custom_cookies)
for raw_propModel in query_wish_item_list_res["data"]["goods"]:
propModel = GoodsInfo()
propModel.auto_update_config(raw_propModel)
# 许愿
param = {
"iActionId": propModel.type,
"iGoodsId": propModel.valiDate[0].code,
"sBizCode": roleModel.bizCode,
}
if roleModel.type == "0":
# 端游
if roleModel.serviceID != "":
param["iZoneId"] = roleModel.serviceID
else:
param["iZoneId"] = roleModel.areaID
param["sZoneDesc"] = quote_plus(roleModel.serviceName)
else:
# 手游
if roleModel.serviceID != "" and roleModel.serviceID != "0":
param["partition"] = roleModel.serviceID
elif roleModel.areaID != "" and roleModel.areaID != "0":
param["partition"] = roleModel.areaID
param["iZoneId"] = roleModel.channelID
if int(roleModel.systemID) < 0:
param["platid"] = 0
else:
param["platid"] = roleModel.systemID
param["sZoneDesc"] = quote_plus(roleModel.serviceName)
if roleModel.bizCode == "lol" and roleModel.accountId != "":
param["sRoleId"] = roleModel.accountId
else:
param["sRoleId"] = roleModel.roleCode
param["sRoleName"] = quote_plus(roleModel.roleName)
param["sGetterDream"] = quote_plus("不要888!不要488!9.98带回家")
wish_res = self.get(
"3.3.3 完成许愿任务", self.urls.make_wish, **param, use_this_cookies=self.djc_custom_cookies
)
# 检查许愿结果
ret = wish_res["ret"]
# 部分情况需要继续尝试下一个