-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.py
1680 lines (1521 loc) · 80.1 KB
/
main.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
import os, re, random, asyncio, hoshino
import base64
from PIL import Image, ImageFont
from io import BytesIO
from hoshino import Service, priv
from hoshino.typing import CQEvent
from hoshino.util import DailyNumberLimiter, FreqLimiter
from domain.entities import *
from domain.services import ExchangeManager
from infrastructure.factories import *
from infrastructure.database.database_init import init_db
from infrastructure.database.connection import AsyncSessionFactory, close_engine
from .group_cd_manager import GroupCDManager
from .utils import (
init_characters,
handle_ex_wife,
download_async,
is_near_midnight,
rename_image_file,
update_single_character,
add_single_character,
backup_character_image,
restore_character_image,
delete_image_and_empty_folder,
get_card_by_uid_gid,
format_seconds,
img_path,
load_ntr_atlas_statuses,
save_ntr_atlas_statuses
)
#————————————————————基本参数————————————————————#
# 创建一个全局的ExchangeManager实例,用于交换老婆和牛老婆的锁
ex_manager = ExchangeManager()
# 初始化GroupCDManager实例, 分群管理日老婆CD
cd_manager = GroupCDManager('group_cd_config.json')
# 群管理员每天可添加老婆的次数
_add_max=1
_add_lmt= DailyNumberLimiter(_add_max)
# 当超出次数时的提示
_add_max_notice = f'为防止滥用,管理员一天最多可添加{_add_max}次,若需更多请用"来杯咖啡"联系维护组'
# 牛老婆的成功率
ntr_possibility = 0.5
# 每人每天可牛老婆的次数
_ntr_max=1
_ntr_lmt= DailyNumberLimiter(_ntr_max)
# 当超出次数时的提示
_ntr_max_notice = f'为防止牛头人泛滥,一天最多可牛{_ntr_max}次,若需更多请用"来杯咖啡"联系维护组'
# 通用命令频率限制,5秒
_flmt = FreqLimiter(5)
# 日老婆频率限制
_mating_lmt = FreqLimiter(1800)
# 档案查询频率限制
_archive_lmt = FreqLimiter(30)
# 每人每天可离婚的次数
_divorce_max=1
_divorce_lmt= DailyNumberLimiter(_divorce_max)
# 当超出次数时的提示
_divorce_max_notice = f'本群每天只允许离婚{_divorce_max}次!'
# 在程序启动时调用:载入NTR图鉴状态
# 用来存储所有群组的NTR图鉴状态的路径和状态
ntr_atlas_status_file = 'ntr_atlas_status.json'
# ——————————————————————图鉴预加载配置——————————————————————#
PRELOAD = True # 是否在启动时直接将所有图片加载到内存中,开启后可以提高查看仓库的速度但会增加内存的占用(大约十几M)
COL_NUM = 10 # 查看仓库时每行显示的卡片个数
# 背景图路径
__BASE = os.path.split(os.path.realpath(__file__))[0]
FRAME_DIR_PATH = os.path.join(__BASE, 'image')
# 老婆路径
DIR_PATH = img_path
# 文字路径,字体大小为16
font = ImageFont.truetype(os.path.join(os.path.dirname(__file__), 'arial.ttf'), 16)
card_file_names_all = []
# 资源预检
image_cache = {}
# 列出DIR_PATH目录下所有的文件和文件夹名,存储在image_list列表中。
# 卡池文件夹名称list
pool_names = [entry for entry in os.listdir(DIR_PATH) if os.path.isdir(os.path.join(DIR_PATH, entry))]
# 遍历每个子文件夹,并加载其中的图片
for pool_name in pool_names:
pool_path = os.path.join(DIR_PATH, pool_name)
image_list = [f for f in os.listdir(pool_path) if os.path.isfile(os.path.join(pool_path, f))]
for image in image_list:
# 去除文件名后缀
file_name_without_ext = os.path.splitext(image)[0]
# 图像缓存
if PRELOAD:
image_path = os.path.join(pool_path, image)
img = Image.open(image_path)
# 缩放图片到80x80像素
img = img.resize((80, 80), Image.ANTIALIAS)
# 转换为RGBA模式
img = img.convert('RGBA')
# 存储到缓存中
image_cache[file_name_without_ext] = img
# 将无后缀的文件名添加到card_file_names_all列表中
card_file_names_all.append(file_name_without_ext)
# 保存输出文件名列表的长度
len_card = len(card_file_names_all)
# 根据给定的图片路径pic_path和是否灰度化的标志grey来返回处理后的图片。
def get_pic(pic_path, grey):
if PRELOAD:
sign_image = image_cache[pic_path]
else:
# 构造基础路径
base_path = os.path.join(DIR_PATH)
# 在每个子文件夹中搜索图片
for pool_name in pool_names:
pool_path = os.path.join(base_path, pool_name)
for entry in os.listdir(pool_path):
if entry.startswith(pic_path):
# 找到图片,加载并处理
image_path = os.path.join(pool_path, entry)
sign_image = Image.open(image_path)
# 图片被缩放到80x80像素,并应用抗锯齿算法
sign_image = sign_image.resize((80, 80), Image.ANTIALIAS)
# 如果grey为True,则将图片转换为灰度图。
if grey:
sign_image = sign_image.convert('L')
return sign_image
#——————————————————————服务——————————————————————#
sv_help = f'''
-[抽老婆] 看看今天的二次元老婆是谁
-[添加老婆+人物名称+卡池名称(选填)+图片] 群管理员每天可以添加一次人物
※为防止bot被封号和数据污染请勿上传太涩与功能无关的图片※
-[交换老婆] @某人 + 交换老婆
-[牛老婆] {ntr_possibility * 100}%概率牛到别人老婆({_ntr_max}次/日)
-[查老婆] 加@某人可以查别人老婆,不加查自己
-[离婚] 清楚当天老婆信息,可以重新抽老婆(管理)
-[重置牛老婆] 加@某人可以重置别人牛的次数(管理)
-[设置日老婆CD] 后接数字(管理)
-[用户档案] 统计用户的一些数据,@某人可以查看他人的数据
-[老婆图鉴] 查看老婆解锁情况,@某人可以查看他人的数据
-[老婆档案] 统计老婆的一些数据,后接老婆名字可以查看具体角色的数据
-[清理抽老婆用户] 清除不在的群和群成员(可能会很卡)(管理)
-[切换NTR图鉴开关状态] 开启图鉴统计NTR所得
———————————————以下仅限管理员—————————————————————————
[更新老婆][删除老婆][重命名老婆]
'''.strip()
sv = Service(
name = '抽老婆', #功能名
use_priv = priv.NORMAL, #使用权限
manage_priv = priv.ADMIN, #管理权限
visible = True, #可见性
enable_on_default = True, #默认启用
bundle = '娱乐', #分组归类
help_ = sv_help #帮助说明
)
#—————————————————————初始化—————————————————————#
# 在插件启动时初始化和创建所需的表
bot = hoshino.get_bot()
@bot.server_app.before_serving
async def initialize_database():
await init_db()
# 角色表为空的时候初始化添加
await init_characters()
# 手动初始化,如果首次启动bot超时可以使用以下方法
# @sv.on_prefix('初始化')
# @sv.on_suffix('初始化')
# async def initialize_database(bot, ev: CQEvent):
# # 获取QQ信息
# user_id = ev.user_id
# # 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
# if user_id not in hoshino.config.SUPERUSERS:
# return
# await init_db()
# # 角色表为空的时候初始化添加
# await init_characters()
# 关闭数据库连接
@bot.server_app.after_serving
async def close_database():
await close_engine()
#—————————————————————抽老婆—————————————————————#
@sv.on_prefix('抽老婆')
@sv.on_suffix('抽老婆')
# @sv.on_fullmatch('抽老婆')
async def darw_wife(bot, ev: CQEvent):
# 获取QQ群、群用户QQid
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}秒后再试')
return
_flmt.start_cd(key)
# 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id in hoshino.config.SUPERUSERS:
# 判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv < sv.manage_priv:
# return
# 这是帮别人抽老婆的功能,如果不需要
# 可以把上面权限相关的,和下面这部分都删除或注释掉
# 然后on_suffix和on_prefix改成on_fullmatch
target_id = None
# 提取目标用户的QQ号
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
# 没有@任何人,且没有附带其他消息,只有“抽老婆”命令
if not target_id and ev.message.extract_plain_text().strip() == "":
pass
elif not target_id:
# 谈论“抽老婆”不做反应
return
else:
user_id=target_id
elif ev.message.extract_plain_text().strip() != "":
# 群友谈论关键词
return
async with AsyncSessionFactory() as session:
async with session.begin():
user_group_sv = await UserGroupSvFactory(session).create()
# 获得user_group,如果没有会自动添加
ug = await user_group_sv.add_and_get_user_group(user_id, group_id)
# 事件记录服务
event_sv = await EventSvFactory(session).create()
# 当前老婆管理服务
current_sv = await CurrentSvFactory(session).create()
# 获得角色,如果不是当天信息,或者角色为空,或者初次使用,返回空
character = await current_sv.get_current_character(ug)
if character:
await event_sv.add_double_event(ug, ug, character, character, "查老婆", "self")
try:
await bot.send(
ev,
f"\n你的今日老婆:\n"
f"{character.pool_name}池:{character.name}\n"
f"{character.image_path}",
at_sender=True
)
except Exception as e:
await bot.send(
ev,
f"\n你的今日老婆:\n"
f"{character.pool_name}池:{character.name}\n"
"[图片发送失败]",
at_sender=True
)
return
# 随机获得一个角色
character_sv = await CharacterSvFactory(session).create()
character = await character_sv.get_random_character()
# ug_character关联表服务
ugc_sv = await UGCharacterSvFactory(session).create()
# 记录是出新还是重复,如果建立过关联就是重复
if await ugc_sv.has_character(ug, character):
result = "重复"
before_stats = await ugc_sv.get_user_group_character_stats(ug, character)
else:
result = "出新"
# 添加ug_character记录,如果已存在会增加一次获得次数
await ugc_sv.add_or_update_character_by_acquisition_method(ug, character, AcqMethod.DRAW)
# 添加到current表
await current_sv.add_or_update_current_character(ug, character)
# 记录抽卡事件
await event_sv.add_single_event(ug, character, "抽老婆", result)
hoshino.logger.info(f"抽到的老婆是:{character.name}")
msg = f"\n{result}了!\n你今天的老婆是:{character.name}哒~\n"
msg_img = f"{character.image_path}"
msg_time = "" # 添加默认值
msg_count = "" # 添加默认值
if result == "重复":
msg_time = f"上次抽到她的时间是:{before_stats.lastest_acquisition_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
msg_count = f"总共抽到她:{before_stats.draw_count + 1}次"
try:
await bot.send(ev, msg + msg_img + msg_time + msg_count, at_sender=True)
except Exception as e:
await bot.send(ev, msg + "[图片发送失败]\n" + msg_time + msg_count, at_sender=True)
#—————————————————————查老婆—————————————————————#
@sv.on_prefix('查老婆')
@sv.on_suffix('查老婆')
async def check_wife(bot, ev: CQEvent):
# 获取QQ群、群用户QQid
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}后再试')
return
_flmt.start_cd(key)
target_id = None
# 提取目标用户的QQ号
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
# 没有@任何人,且没有附带其他消息,只有“查老婆”命令
if not target_id and ev.message.extract_plain_text().strip() == "":
# 查自己
target_id = user_id
lookup_type = "self"
elif not target_id:
# 谈论“查老婆”不做反应
return
else:
lookup_type = "other"
async with AsyncSessionFactory() as session:
async with session.begin():
user_group_sv = await UserGroupSvFactory(session).create() # ug服务
event_sv = await EventSvFactory(session).create() # event服务
current_ugc_sv = await CurrentSvFactory(session).create() # 当天角色
# 获得ug对应的当前角色信息
ug = await user_group_sv.add_and_get_user_group(user_id, group_id)
character = await current_ugc_sv.get_current_character(ug)
if lookup_type == "self":
# 查自己的情况, 如果有老婆
if character:
ugc_sv = await UGCharacterSvFactory(session).create() # ug_c服务
stats = await ugc_sv.get_user_group_character_stats(ug, character)
try:
await bot.send(
ev,
"\n今日老婆:\n"
f"{character.pool_name}池:{character.name}\n"
f"{character.image_path}"
f"抽到:{stats.draw_count}次\n"
f"交换得到:{stats.acquired_by_ex_count}次\n"
f"牛到:{stats.acquired_by_ntr_count}次\n"
f"好感度:{stats.mating_count}\n"
f"离婚次数:{stats.divorce_count}\n"
f"今日获得时间:{stats.lastest_acquisition_time.strftime('%H:%M:%S')}",
at_sender=True
)
except Exception as e:
await bot.send(
ev,
"\n今日老婆:\n"
f"{character.pool_name}池:{character.name}\n"
"[图片发送失败]\n"
f"抽到:{stats.draw_count}次\n"
f"交换得到:{stats.acquired_by_ex_count}次\n"
f"牛到:{stats.acquired_by_ntr_count}次\n"
f"好感度:{stats.mating_count}\n"
f"离婚次数:{stats.divorce_count}\n"
f"今日获得时间:{stats.lastest_acquisition_time.strftime('%H:%M:%S')}",
at_sender=True
)
# 记录抽老婆结果
await event_sv.add_double_event(ug, ug, character, character, "查老婆", lookup_type)
else:
await bot.send(ev, "今天还没抽老婆哦~", at_sender=True)
else:
ug_target = await user_group_sv.add_and_get_user_group(target_id, group_id)
# 获得ug_target对应的当前角色信息
character_target = await current_ugc_sv.get_current_character(ug_target)
nick_name = await get_card_by_uid_gid(target_id, group_id)
if character_target:
ugc_sv = await UGCharacterSvFactory(session).create()
stats = await ugc_sv.get_user_group_character_stats(ug_target, character_target)
try:
await bot.send(
ev,
f"\n{nick_name}的今日老婆:\n"
f"{character_target.pool_name}池:{character_target.name}\n"
f"{character_target.image_path}"
f"抽到:{stats.draw_count}次\n"
f"交换得到:{stats.acquired_by_ex_count}次\n"
f"牛到:{stats.acquired_by_ntr_count}次\n"
f"好感度:{stats.mating_count}\n"
f"离婚次数:{stats.divorce_count}\n"
f"今日获得时间:{stats.lastest_acquisition_time.strftime('%H:%M:%S')}",
at_sender=True
)
except Exception as e:
await bot.send(
ev,
f"\n{nick_name}的今日老婆:\n"
f"{character_target.pool_name}池:{character_target.name}\n"
"[图片发送失败]\n"
f"抽到:{stats.draw_count}次\n"
f"交换得到:{stats.acquired_by_ex_count}次\n"
f"牛到:{stats.acquired_by_ntr_count}次\n"
f"好感度:{stats.mating_count}\n"
f"离婚次数:{stats.divorce_count}\n"
f"今日获得时间:{stats.lastest_acquisition_time.strftime('%H:%M:%S')}",
at_sender=True
)
# 记录抽老婆结果
await event_sv.add_double_event(ug, ug_target, character, character_target, "查老婆", lookup_type)
else:
await bot.send(ev, f"\n{nick_name}今天还没抽老婆哦~", at_sender=True)
#———————————————————交换老婆—————————————————————#
@sv.on_prefix('交换老婆')
@sv.on_suffix('交换老婆')
async def exchange_wife(bot, ev: CQEvent):
if await is_near_midnight():
await bot.send(ev, '日期即将变更,请第二天再进行交换', at_sender=True)
return
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}秒后再试')
return
_flmt.start_cd(key)
target_id = None
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
if not target_id:
if ev.message.extract_plain_text().strip() != "":
# 谈论交换老婆
return
await bot.send(ev, '你想和谁交换?请@他', at_sender=True)
return
# 检查是否尝试交换给自己
if user_id == target_id:
await bot.send(ev, '左手换右手?', at_sender=True)
return
# 检查发起者或目标者是否已经在任何交换请求中
if await ex_manager.is_exchange_active(user_id, target_id, group_id):
await bot.send(ev, '双方有人正在进行换妻play中,请稍后再试', at_sender=True)
return
async with AsyncSessionFactory() as session:
async with session.begin():
ug_sv = await UserGroupSvFactory(session).create()
ug = await ug_sv.add_and_get_user_group(user_id, group_id)
ug_target = await ug_sv.add_and_get_user_group(target_id, group_id)
# 获得双方的当前老婆信息
current_sv = await CurrentSvFactory(session).create()
ug_wife = await current_sv.get_current_character(ug)
ug_target_wife = await current_sv.get_current_character(ug_target)
if not ug_wife or not ug_target_wife:
await bot.send(ev, '需要双方都有老婆才能交换', at_sender=True)
return
# 添加交易状态锁,防止交易期间他人再对双方发起交易,或重复发起交易
await ex_manager.add_exchange(user_id, target_id, group_id)
# 启动超时计时器
asyncio.create_task(handle_timeout(ug, ug_target, ug_wife, ug_target_wife))
# 发送交换请求
await bot.send(ev, f'[CQ:at,qq={target_id}] 用户 [CQ:at,qq={user_id}] 想要和你交换老婆,是否同意?\n如果同意(拒绝)请在60秒内发送“同意(拒绝)”', at_sender=False)
# 交换老婆超时处理
async def handle_timeout(ug: UserGroup, ug_target: UserGroup, wife: Character, wife_target: Character):
await asyncio.sleep(60)
if await ex_manager.get_initiator_if_target(ug_target.user_id, ug_target.group_id):
async with AsyncSessionFactory() as session:
async with session.begin():
event_sv = await EventSvFactory(session).create()
await event_sv.add_double_event(ug, ug_target, wife, wife_target, "交换老婆", "超时")
await hoshino.get_bot().send_group_msg(group_id=ug.group_id, message=f"[CQ:at,qq={ug.user_id}] 你的交换请求已超时,对方无视了你")
await ex_manager.remove_exchange(ug.user_id, ug.group_id)
# 交换老婆回复处理
@sv.on_message('group')
async def ex_wife_reply(bot, ev: CQEvent):
# 如果该群组内没有交换请求
if not await ex_manager.has_active_exchanges_in_group(ev.group_id):
return
# 存在交换请求
group_id = ev.group_id
target_id = ev.user_id
# 判断该用户是否是被申请者,返回申请者id就说明是
user_id = await ex_manager.get_initiator_if_target(target_id, group_id)
# 不为空则说明有记录
if user_id:
# 提取消息文本
keyword = "".join(seg.data['text'].strip() for seg in ev.message if seg.type == 'text')
# 寻找关键词的索引位置
agree_index = keyword.find('同意')
disagree_index = keyword.find('不同意')
refuse_index = keyword.find('拒绝')
# 如果“同意”、“不同意”和“拒绝”都没有出现,则什么都不做
if agree_index == -1 and disagree_index == -1 and refuse_index == -1:
return
# 找出“不同意”和“拒绝”首次出现的位置
disagree_first_index = min(filter(lambda x: x != -1, [disagree_index, refuse_index]), default=-1)
# 进行判断
if disagree_first_index != -1 and (agree_index == -1 or disagree_first_index < agree_index):
# 如果找到“不同意”或“拒绝”,且它们在“同意”之前出现,或者没找到“同意”
await handle_ex_wife(user_id, target_id, group_id, False)
await bot.send(ev, '对方拒绝了你的交换请求', at_sender=True)
elif agree_index != -1:
# 如果找到“同意”,且它在“不同意”或“拒绝”之前出现,或者“不同意”或“拒绝”不存在
await handle_ex_wife(user_id, target_id, group_id, True)
await bot.send(ev, '交换成功', at_sender=True)
# 删除exchange_manager中该用户的请求
await ex_manager.remove_exchange(user_id, group_id)
#—————————————————————牛老婆—————————————————————#
@sv.on_prefix('牛老婆')
@sv.on_suffix('牛老婆')
async def ntr_wife(bot, ev: CQEvent):
if await is_near_midnight():
await bot.send(ev, '日期即将变更,请第二天再牛', at_sender=True)
return
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}秒后再试')
return
_flmt.start_cd(key)
# 牛老婆次数限制
if not _ntr_lmt.check(key):
await bot.send(ev, _ntr_max_notice, at_sender=True)
return
target_id = None
# 提取目标用户的QQ号
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
if not target_id:
if ev.message.extract_plain_text().strip() != "":
return
await bot.send(ev, '请@一个要下手的目标', at_sender=True)
return
# 检查是否尝试交换给自己
if user_id == target_id:
await bot.send(ev, '不能牛自己', at_sender=True)
return
# 检查发起者或目标者是否已经在任何交换请求中
if await ex_manager.is_exchange_active(user_id, target_id, group_id):
await bot.send(ev, '双方有人正在进行换妻play中,请稍后再牛', at_sender=True)
return
async with AsyncSessionFactory() as session:
async with session.begin():
ug_sv = await UserGroupSvFactory(session).create()
ug = await ug_sv.add_and_get_user_group(user_id, group_id)
ug_target = await ug_sv.add_and_get_user_group(target_id, group_id)
# 获得对方的当前老婆信息
current_sv = await CurrentSvFactory(session).create()
ug_wife = await current_sv.get_current_character(ug)
ug_target_wife = await current_sv.get_current_character(ug_target)
# 检查对方是否有老婆
if not ug_target_wife:
await bot.send(ev, '需要对方有老婆才能牛', at_sender=True)
return
# 满足牛人条件,添加进交换请求列表中,防止牛人期间他人对双方发起交易,产生bug
await ex_manager.add_exchange(user_id, target_id, group_id)
# 事件记录服务
event_sv = await EventSvFactory(session).create()
if random.random() < ntr_possibility:
# 记录一次“牛老婆”动作,成功
await event_sv.add_double_event(ug, ug_target, ug_wife, ug_target_wife, "牛老婆", "成功")
# user相当于通过牛老婆的方式抽到一个新老婆
ugc_sv = await UGCharacterSvFactory(session).create()
# 建立关联,或者增加一次牛老婆获得次数
await ugc_sv.add_or_update_character_by_acquisition_method(ug, ug_target_wife, AcqMethod.NTR)
# 更新当前老婆
await current_sv.add_or_update_current_character(ug, ug_target_wife)
# 目标用户当前老婆置空
await current_sv.remove_cid_by_user_group(ug_target)
nick = await get_card_by_uid_gid(
user_id=target_id,
group_id=group_id
)
await bot.send(ev, f'你的阴谋已成功!已成功将 {nick} 的老婆占为己有', at_sender=True)
# 被牛走的人,补偿一次牛老婆机会
_ntr_lmt.increase(f"{ug_target.user_id}_{group_id}", int(-1))
else:
# 记录一次“牛老婆”动作,失败
await event_sv.add_double_event(ug, ug_target, ug_wife, ug_target_wife, "牛老婆", "失败")
await bot.send(ev, f'你的阴谋失败了,黄毛被干掉了,黄毛被干掉了!你还有{_ntr_max - _ntr_lmt.get_num(key)}条命', at_sender=True)
# 清除交换请求锁
await ex_manager.remove_exchange(user_id, group_id)
# 牛老婆次数减少
_ntr_lmt.increase(key)
# 重置牛老婆次数限制
@sv.on_prefix('重置牛老婆')
@sv.on_suffix('重置牛老婆')
async def reset_ntr_wife(bot, ev: CQEvent):
# 获取QQ信息
user_id = ev.user_id
group_id = ev.group_id
# # 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id not in hoshino.config.SUPERUSERS:
await bot.send(ev,"该功能仅限bot管理员使用")
return
# 判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv < sv.manage_priv:
# await bot.send(ev,"该功能仅限群管理员或为bot设置的超级管理员使用")
# return
target_id = None
# 提取目标用户的QQ号
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
target_id = target_id or user_id
_ntr_lmt.reset(f"{target_id}_{group_id}")
await bot.send(ev,"已重置次数")
#————————————————————日老婆——————————————————————#
@sv.on_fullmatch('日老婆')
async def mating_wife(bot, ev: CQEvent):
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}秒后再试')
return
_flmt.start_cd(key)
# 日老婆CD
if not _mating_lmt.check(key):
await bot.send(ev, f'贤者时间,请等待{format_seconds(_mating_lmt.left_time(key))}')
return
# 获得当日老婆
async with AsyncSessionFactory() as session:
async with session.begin():
try:
ug_sv = await UserGroupSvFactory(session).create()
ug = await ug_sv.add_and_get_user_group(user_id, group_id)
current_sv = await CurrentSvFactory(session).create()
ug_wife = await current_sv.get_current_character(ug)
if not ug_wife:
await bot.send(ev, "日天日地日空气?请得到老婆后再日")
return
action_sv = await ActionSvFactory(session).create()
# 事件记录服务
event_sv = await EventSvFactory(session).create()
await event_sv.add_single_event(ug, ug_wife, "日老婆", "")
# 日老婆次数加一
await action_sv.update_action_count(ug, ug_wife, ActionType.MATING)
except Exception as e:
await bot.send(ev, "注入失败!")
hoshino.logger.error(f"日老婆异常: {e}")
return
await bot.send(ev, f"誰にでも優しくしないで, でもそこが好き!\n({ug_wife.name}好感度加一)")
# 获取特定群组的CD时间
cd_time = cd_manager.get_group_cd(f"{group_id}")
# 日老婆CD
_mating_lmt.start_cd(key, cd_time)
#———————————————————添加老婆—————————————————————#
@sv.on_prefix('添加老婆')
@sv.on_suffix('添加老婆')
async def add_wife(bot, ev: CQEvent):
# 获取QQ信息
user_id = ev.user_id
group_id = ev.group_id
key = f"{user_id}_{group_id}"
# 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id not in hoshino.config.SUPERUSERS:
return
#判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv < sv.manage_priv:
# return
# elif not _add_lmt.check(key):
# # 检查用户今天是否已添加过老婆信息
# await bot.send(ev, _add_max_notice, at_sender=True)
# return
message_text = ev.message.extract_plain_text().strip()
keywords = message_text.split()
if len(keywords) == 1:
# 只有一个关键词时,默认卡池名为 default
name = keywords[0]
pool_name = 'default'
elif len(keywords) == 2:
# 有两个关键词时,正常处理,提取老婆的名字和卡池名
name, pool_name = keywords
else:
await bot.send(
ev,
f"请提供老婆名和可选的卡池名(默认default)\n"
f"卡池名称需'纯英文'且为'小写字母'或下划线'_'\n"
f"老婆名和卡池名用空格隔开,如:\n"
f"添加老婆 老婆名 [图片]\n"
f"添加老婆 老婆名 swimwear [图片]")
return
# 检测卡池名是否是纯英文且为小写字母或下划线
if not re.match("^[a-z_]+$", pool_name) or len(pool_name) > 30:
await bot.send(ev, "卡池名需为纯英文小写字母或下划线'_',且长度不超过30个字符,请重新输入。")
# 获得图片信息
ret = re.search(r"\[CQ:image,file=(.*)?,url=(.*)\]", str(ev.message))
if not ret:
# 未获得图片信息
await bot.send(ev, '请附带二次元老婆图片~')
return
# 检查是否同名,同名禁止添加,需要到更新老婆去更新
async with AsyncSessionFactory() as session:
async with session.begin():
character_sv = await CharacterSvFactory(session).create()
existing_character = await character_sv.get_character_by_name(name)
if existing_character:
await bot.send(ev, '出现重名,请使用[更新老婆]命令')
return
# 获取下载url
url = ret.group(2)
# 下载图片保存到本地并获取文件名
try:
image_name = await download_async(url, name, pool_name)
except Exception as e:
hoshino.logger.exception(f'下载图片失败:{e}')
await bot.send(ev, '下载图片失败')
return
# 插入数据库
try:
character_sv = await CharacterSvFactory(session).create()
await add_single_character(character_sv, image_name, pool_name)
except Exception as e:
hoshino.logger.exception(f'添加新老婆信息失败:{e}')
await bot.send(ev, '添加新老婆信息失败')
return
await bot.send(ev, '信息已增加~')
# 如果不是超级管理员,增加用户的添加老婆使用次数(管理员可一天增加多次)
if user_id not in hoshino.config.SUPERUSERS:
_add_lmt.increase(key)
#———————————————————更新老婆—————————————————————#
# 同名则更新pool_name和image_name、image_path(cqcode)
@sv.on_prefix('更新老婆')
@sv.on_suffix('更新老婆')
async def update_wife(bot, ev: CQEvent):
# 获取QQ信息
user_id = ev.user_id
# 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id not in hoshino.config.SUPERUSERS:
return
# 判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv < sv.manage_priv:
# return
message_text = ev.message.extract_plain_text().strip()
keywords = message_text.split()
if len(keywords) == 1:
# 只有一个关键词时,默认卡池名为 原卡池名
name = keywords[0]
pool_name = ""
elif len(keywords) == 2:
# 有两个关键词时,正常处理,提取老婆的名字和卡池名
name, pool_name = keywords
else:
await bot.send(
ev,
f"请提供老婆名和可选的卡池名(默认原卡池名)\n"
f"卡池名称需'纯英文'且为'小写字母'或下划线'_'\n"
f"老婆名和卡池名用空格隔开,如:\n"
f"更新老婆 老婆名 [图片]\n"
f"更新老婆 老婆名 swimwear [图片]")
return
# 检测卡池名是否是纯英文且为小写字母或下划线
if not re.match("^[a-z_]*$", pool_name) or len(pool_name) > 30:
await bot.send(ev, "卡池名需为纯英文小写字母或下划线'_',且长度不超过30个字符,请重新输入。")
return
# 获得图片信息
ret = re.search(r"\[CQ:image,file=(.*)?,url=(.*)\]", str(ev.message))
if not ret:
# 未获得图片信息
await bot.send(ev, '请附带二次元老婆图片~')
return
# 检查是否同名,不同名让他去添加,同名就更新
async with AsyncSessionFactory() as session:
async with session.begin():
character_sv = await CharacterSvFactory(session).create()
existing_character = await character_sv.get_character_by_name(name)
if not existing_character:
await bot.send(ev, '同名角色不存在,请使用[添加老婆]命令')
return
if pool_name == "":
pool_name = existing_character.pool_name
# 备份原图片文件
backup_path, src_path, backup_image_name, backup_pool_name = await backup_character_image(existing_character.image_name, existing_character.pool_name)
# 下载新图片文件
url = ret.group(2)
try:
new_image_name = await download_async(url, name, pool_name)
except Exception as e:
# 恢复原图片文件
await restore_character_image(backup_path, src_path)
hoshino.logger.exception(f'下载图片失败: {e}')
await bot.send(ev, '下载图片失败')
return
# 更新信息
try:
await update_single_character(character_sv, existing_character, new_image_name, pool_name)
# 删除备份的原图片
await delete_image_and_empty_folder(backup_image_name, backup_pool_name)
await bot.send(ev, '更新信息成功')
except Exception as e:
# 删除下载的新图片
await delete_image_and_empty_folder(new_image_name, pool_name)
# 恢复原图片文件
await restore_character_image(backup_path, src_path)
hoshino.logger.exception(f'更改角色信息数据失败: {e}')
await bot.send(ev, '更改角色信息数据失败')
# 重命名老婆
@sv.on_prefix('重命名老婆')
@sv.on_suffix('重命名老婆')
async def rename_wife(bot, ev: CQEvent):
user_id = ev.user_id
# 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id not in hoshino.config.SUPERUSERS:
return
message_text = ev.message.extract_plain_text().strip()
keywords = message_text.split()
if len(keywords) != 2:
await bot.send(
ev,
f"请提供当前老婆名和新老婆名,用空格隔开,如:\n"
f"重命名老婆 当前老婆名 新老婆名")
return
old_name, new_name = keywords
async with AsyncSessionFactory() as session:
async with session.begin():
character_sv = await CharacterSvFactory(session).create()
existing_character = await character_sv.get_character_by_name(old_name)
if not existing_character:
await bot.send(ev, '当前老婆名不存在,请确认后重新输入')
return
# 重命名文件
old_image_name_extension = os.path.splitext(existing_character.image_name)[1]
new_image_name = f"{new_name}{old_image_name_extension}"
try:
await rename_image_file(existing_character.image_name, new_image_name, existing_character.pool_name)
except (FileNotFoundError, OSError) as e:
hoshino.logger.exception(f'重命名图片文件失败: {e}')
await bot.send(ev, '重命名失败')
return
# 更新数据库信息
try:
await update_single_character(character_sv, existing_character, new_image_name, existing_character.pool_name)
await bot.send(ev, f"成功将老婆名从 {old_name} 更改为 {new_name}")
except Exception as e:
# 如果更新数据库失败,恢复文件名
try:
await rename_image_file(new_image_name, existing_character.image_name, existing_character.pool_name)
except (FileNotFoundError, OSError) as rename_error:
hoshino.logger.exception(f'恢复图片文件名失败: {rename_error}')
await bot.send(ev, '重命名失败,且恢复原文件名失败')
return
hoshino.logger.exception(f'更新角色信息失败: {e}')
await bot.send(ev, '更新角色信息失败')
#———————————————————删除老婆—————————————————————#
@sv.on_prefix('删除老婆')
@sv.on_suffix('删除老婆')
async def remove_wife(bot, ev: CQEvent):
# 获取QQ信息
user_id = ev.user_id
# 此注释的代码是仅限bot超级管理员使用,有需可启用并将下面判断权限的代码注释掉
if user_id not in hoshino.config.SUPERUSERS:
return
# 判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv < sv.manage_priv:
# return
# 获得要删除的名字
name = ev.message.extract_plain_text().strip()
async with AsyncSessionFactory() as session:
async with session.begin():
character_sv = await CharacterSvFactory(session).create()
try:
image_name, pool_name = await character_sv.delete_charactera_by_name(name)
await delete_image_and_empty_folder(image_name, pool_name)
await bot.send(ev, f"删除 {name} 成功")
except Exception as e:
hoshino.logger.exception(f"删除角色失败:{e}")
await bot.send(ev, f"未找到 {name}")
#———————————————————离婚老婆—————————————————————#
@sv.on_prefix('离婚')
@sv.on_suffix('离婚')
async def reset_darw_wife(bot, ev: CQEvent):
# 获取QQ群、群用户QQid
group_id = ev.group_id
user_id = ev.user_id
# 命令频率限制
key = f"{user_id}_{group_id}"
if not _flmt.check(key):
await bot.send(ev, f'操作太频繁,请在{int(_flmt.left_time(key))}秒后再试')
return
_flmt.start_cd(key)
# 离婚次数限制
if not _divorce_lmt.check(key):
await bot.send(ev, _divorce_max_notice, at_sender=True)
return
# 此注释的代码是仅限bot超级管理员可以帮别人离婚,有需可启用并将下面判断权限的代码注释掉
if user_id in hoshino.config.SUPERUSERS:
# 判断权限,只有用户为群管理员或为bot设置的超级管理员才能使用
# u_priv = priv.get_user_priv(ev)
# if u_priv > sv.manage_priv:
# 和抽老婆一样,帮别人离婚的
target_id = None
# 提取目标用户的QQ号
for seg in ev.message:
if seg.type == 'at' and seg.data['qq'] != 'all':
target_id = int(seg.data['qq'])
break
# 没有@任何人,且没有附带其他消息,只有“离婚”命令
if not target_id and ev.message.extract_plain_text().strip() == "":
pass
elif not target_id:
# 谈论“离婚”不做反应
return
else:
user_id=target_id
elif ev.message.extract_plain_text().strip() != "":
# 群友谈论离婚
return
async with AsyncSessionFactory() as session:
async with session.begin():
user_group_sv = await UserGroupSvFactory(session).create()
# 获得user_group,如果没有会自动添加
ug = await user_group_sv.add_and_get_user_group(user_id, group_id)
# 获得ug对应的当前角色信息
current_ugc_sv = await CurrentSvFactory(session).create()
character = await current_ugc_sv.get_current_character(ug)
if character:
# 清除老婆信息
await current_ugc_sv.remove_cid_by_user_group(ug)
stats_sv = await ActionSvFactory(session).create()
await stats_sv.update_action_count(ug, character, ActionType.DIVORCE)
# 事件记录服务
event_sv = await EventSvFactory(session).create()
await event_sv.add_single_event(ug, character, "离婚", "")
await bot.send(ev, f"当前老婆信息:{character.name}, 解除婚姻成功", at_sender=True)
if ev.user_id not in hoshino.config.SUPERUSERS:
# 不是超级管理员,限制次数
_divorce_lmt.increase(key)
else:
await bot.send(ev, f"没老婆不能离婚", at_sender=True)