├── utils ├── __init__.py ├── file │ ├── __init_.py │ ├── files.py │ └── fileManage.py ├── gTime.py ├── myLog.py ├── apiHandler.py └── botVip.py ├── config ├── log.exp │ ├── VipUser.json │ └── AfdWebhook.json └── config.exp.json ├── requirements.txt ├── img ├── 643fce843df92.png ├── 643fceb15227a.png ├── 643fcec9808af.png ├── 643fcf10cb956.png ├── 643fd191e4690.png ├── 643fd1a45f307.png ├── 643fd1afc1a49.png ├── 643fd23c6dff9.png ├── 643fd345357d4.png ├── 643fd4979854c.png ├── 643fd4a6a408b.png ├── 643fd53a4804f.png ├── 643fd60fbabe7.png ├── 643fd65f46258.png ├── 643fd6ef0ec7e.png ├── 643fd7012ccc0.png ├── 643fd71a104c1.png ├── 643ffb22e93a1.png └── 643ffb303e7c4.png ├── .gitignore ├── start.py ├── LICENSE ├── api.py ├── main.py └── README.md /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/file/__init_.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/log.exp/VipUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "data":{} 3 | } -------------------------------------------------------------------------------- /config/log.exp/AfdWebhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "data":{}, 3 | "user":{} 4 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.1.0 2 | aiohttp==3.8.1 3 | khl.py==0.3.7 4 | -------------------------------------------------------------------------------- /img/643fce843df92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fce843df92.png -------------------------------------------------------------------------------- /img/643fceb15227a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fceb15227a.png -------------------------------------------------------------------------------- /img/643fcec9808af.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fcec9808af.png -------------------------------------------------------------------------------- /img/643fcf10cb956.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fcf10cb956.png -------------------------------------------------------------------------------- /img/643fd191e4690.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd191e4690.png -------------------------------------------------------------------------------- /img/643fd1a45f307.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd1a45f307.png -------------------------------------------------------------------------------- /img/643fd1afc1a49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd1afc1a49.png -------------------------------------------------------------------------------- /img/643fd23c6dff9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd23c6dff9.png -------------------------------------------------------------------------------- /img/643fd345357d4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd345357d4.png -------------------------------------------------------------------------------- /img/643fd4979854c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd4979854c.png -------------------------------------------------------------------------------- /img/643fd4a6a408b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd4a6a408b.png -------------------------------------------------------------------------------- /img/643fd53a4804f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd53a4804f.png -------------------------------------------------------------------------------- /img/643fd60fbabe7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd60fbabe7.png -------------------------------------------------------------------------------- /img/643fd65f46258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd65f46258.png -------------------------------------------------------------------------------- /img/643fd6ef0ec7e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd6ef0ec7e.png -------------------------------------------------------------------------------- /img/643fd7012ccc0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd7012ccc0.png -------------------------------------------------------------------------------- /img/643fd71a104c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643fd71a104c1.png -------------------------------------------------------------------------------- /img/643ffb22e93a1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643ffb22e93a1.png -------------------------------------------------------------------------------- /img/643ffb303e7c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/musnows/Kook-Afd-Webhook-Bot/HEAD/img/643ffb303e7c4.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # config file 2 | config.json 3 | 4 | # log file 5 | log/ 6 | *.log 7 | 8 | # python file 9 | __pycache__/ 10 | 11 | # idea 12 | .idea/ 13 | .vscode/ -------------------------------------------------------------------------------- /utils/gTime.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime,timedelta,timezone 2 | 3 | def getTime(format_str='%y-%m-%d %H:%M:%S'): 4 | """获取当前时间,默认格式为 `23-01-01 00:00:00`""" 5 | utc_dt = datetime.now(timezone.utc) # 获取当前时间 6 | bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8))) # 转换为北京时间 7 | return bj_dt.strftime(format_str) 8 | -------------------------------------------------------------------------------- /config/config.exp.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot": { 3 | "token": "bot webhook/websocket token", 4 | "verify_token": "bot webhook verify token", 5 | "encrypt": "bot webhook encrypt token", 6 | "webhook_port": 40000, 7 | "ws": false, 8 | "info": "set 'ws' to false if using webhook" 9 | }, 10 | "channel": { 11 | "debug_ch": "channel id for sending debug msg" 12 | } 13 | } -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | from main import bot,_log 4 | from api import app 5 | 6 | # 屏蔽报错 7 | # ignore warning 'DeprecationWarning: There is no current event loop' 8 | import warnings 9 | warnings.filterwarnings("ignore", category=DeprecationWarning) 10 | 11 | if __name__ == '__main__': 12 | HOST,PORT = '0.0.0.0',14726 13 | _log.info(f"[START] service start at {HOST}:{PORT}") 14 | asyncio.get_event_loop().run_until_complete( 15 | asyncio.gather(web._run_app(app, host=HOST, port=PORT), bot.start())) 16 | -------------------------------------------------------------------------------- /utils/file/files.py: -------------------------------------------------------------------------------- 1 | from .fileManage import FileManage 2 | from ..myLog import _log 3 | 4 | # 配置相关 5 | config = FileManage("./config/config.json", True) 6 | """机器人配置文件""" 7 | AfdWebhook = FileManage("./log/AfdWebhook.json") 8 | """爱发电的wh请求""" 9 | 10 | # vip相关 11 | VipUser = FileManage("./log/VipUser.json") 12 | """vip 用户列表/抽奖记录""" 13 | VipUserDict = VipUser['data'] 14 | """vip 用户列表""" 15 | 16 | # 实例化一个khl的bot,方便其他模组调用 17 | from khl import Bot,Cert 18 | bot = Bot(token=config['bot']['token']) # websocket 19 | """main bot""" 20 | if not config['bot']['ws']: # webhook 21 | bot = Bot(cert=Cert(token=config['bot']['token'], 22 | verify_token=config['bot']['verify_token'], 23 | encrypt_key=config['bot']['encrypt']), 24 | port=config['bot']['webhook_port']) # webhook 25 | """main bot""" 26 | _log.info(f"Loading all files") # 走到这里代表所有文件都打开了 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 musnow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/myLog.py: -------------------------------------------------------------------------------- 1 | import logging # 采用logging来替换所有print 2 | LOGGER_NAME = "botlog" 3 | LOGGER_FILE = "bot.log" 4 | 5 | # 只打印info以上的日志(debug低于info) 6 | logging.basicConfig(level=logging.INFO, 7 | format="[%(asctime)s] %(levelname)s:%(filename)s:%(funcName)s:%(lineno)d | %(message)s", 8 | datefmt="%y-%m-%d %H:%M:%S") 9 | # 获取一个logger对象 10 | _log = logging.getLogger(LOGGER_NAME) 11 | # 实例化控制台handler和文件handler,同时输出到控制台和文件 12 | # cmd_handler = logging.StreamHandler() # 默认设置里面,就会往控制台打印信息;自己又加一个,导致打印俩次 13 | file_handler = logging.FileHandler(LOGGER_FILE, mode="a", encoding="utf-8") 14 | fmt = logging.Formatter(fmt="[%(asctime)s] %(levelname)s:%(filename)s:%(funcName)s:%(lineno)d | %(message)s", 15 | datefmt="%y-%m-%d %H:%M:%S") 16 | file_handler.setFormatter(fmt) 17 | _log.addHandler(file_handler) 18 | 19 | 20 | # 在控制台打印msg内容,用作日志 21 | from khl import Message,PrivateMessage 22 | def logMsg(msg: Message) -> None: 23 | try: 24 | # 系统消息id,直接退出,不记录 25 | if msg.author_id == "3900775823":return 26 | # 私聊用户没有频道和服务器id 27 | if isinstance(msg, PrivateMessage): 28 | _log.info( 29 | f"PrivateMsg | Au:{msg.author_id} {msg.author.username}#{msg.author.identify_num} | {msg.content}") 30 | else: 31 | _log.info( 32 | f"G:{msg.ctx.guild.id} | C:{msg.ctx.channel.id} | Au:{msg.author_id} {msg.author.username}#{msg.author.identify_num} = {msg.content}" 33 | ) 34 | except: 35 | _log.exception("Exception occurred") -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from aiohttp import web 3 | 4 | from utils.gTime import getTime 5 | from utils.myLog import _log 6 | from utils.apiHandler import afd_request 7 | 8 | # 初始化节点 9 | routes = web.RouteTableDef() 10 | 11 | 12 | # 基础返回 13 | @routes.get('/') 14 | async def hello_world(request): # put application's code here 15 | _log.info(f"request | root-url") 16 | return web.Response(body=json.dumps( 17 | { 18 | 'code': 0, 19 | 'message': f'Hello! Get recv at {getTime()}' 20 | }, 21 | indent=2, 22 | sort_keys=True, 23 | ensure_ascii=False), 24 | status=200, 25 | content_type='application/json') 26 | 27 | 28 | # 爱发电的wh 29 | @routes.post('/afd') 30 | async def aifadian_webhook(request): 31 | _log.info(f"request | /afd") 32 | try: 33 | ret = await afd_request(request) 34 | return web.Response(body=json.dumps(ret, indent=2, sort_keys=True, ensure_ascii=False), 35 | content_type='application/json') 36 | except: 37 | _log.exception("Exception in /afd") 38 | return web.Response(body=json.dumps({ 39 | "ec": 0, 40 | "em": "err ouccer" 41 | }, indent=2, sort_keys=True, ensure_ascii=False), 42 | status=503, 43 | content_type='application/json') 44 | 45 | 46 | 47 | 48 | app = web.Application() 49 | app.add_routes(routes) 50 | if __name__ == '__main__': 51 | try: # host需要设置成0.0.0.0,否则只有本地才能访问 52 | HOST,PORT = '0.0.0.0',14726 53 | _log.info(f"API Service Start at {HOST}:{PORT}") 54 | web.run_app(app, host=HOST, port=PORT) 55 | except: 56 | _log.exception("Exception occur") -------------------------------------------------------------------------------- /utils/apiHandler.py: -------------------------------------------------------------------------------- 1 | import json 2 | from khl.card import CardMessage,Card,Module,Element,Types 3 | 4 | from .myLog import _log 5 | from .file.files import AfdWebhook,config,bot 6 | from .botVip import add_vip 7 | 8 | def get_order_id_dict(custom_order_id:str)->dict: 9 | """解析custom_order_id""" 10 | index = custom_order_id.find(':') 11 | user_id = custom_order_id[:index] 12 | day = custom_order_id[index+1:] 13 | day = int(day) 14 | return {"uid":user_id,"day":day} 15 | 16 | # 测试一下这个函数 17 | # print(get_order_id_dict("1342354:30")) 18 | 19 | async def afd_request(request): 20 | """爱发电webhook处理函数""" 21 | # 获取参数信息 22 | body = await request.content.read() 23 | params = json.loads(body.decode('UTF8')) 24 | # 插入到日志中 25 | global AfdWebhook 26 | if "data" not in AfdWebhook: 27 | AfdWebhook["data"] = [] 28 | AfdWebhook['data'].append(params) 29 | # 构造text 30 | text = "" 31 | if 'plan_title' in params['data']['order']: 32 | text = f"商品 {params['data']['order']['plan_title']}\n" 33 | user_id = params['data']['order']['user_id'] # afd用户id 34 | user_id = user_id[0:6] 35 | text += f"用户 {user_id}\n" 36 | for i in params['data']['order']['sku_detail']: 37 | text += f"发电了{i['count']}个 {i['name']}\n" 38 | text += f"共计 {params['data']['order']['total_amount']} 猿\n" 39 | # 处理自定义订单编号 40 | if 'custom_order_id' in params['data']['order']: 41 | text += f"自定义订单ID {params['data']['order']['custom_order_id']}" 42 | # kook用户id:vip天数 43 | order_id = params['data']['order']['custom_order_id'] 44 | order_id = get_order_id_dict(order_id) 45 | _log.info(f"[afd] {str(order_id)}") 46 | if 'user' not in AfdWebhook: 47 | AfdWebhook['user'] = {} 48 | if order_id['uid'] not in AfdWebhook['user']: 49 | AfdWebhook['user'][order_id['uid']] = {} 50 | # 配置信息 51 | AfdWebhook['user'][order_id['uid']][params['data']['order']['out_trade_no']] = { 52 | "days":order_id['day'], 53 | "plan_id":params['data']['order']['plan_id'], 54 | "plan_title":params['data']['order']['plan_title'], 55 | "amount":params['data']['order']['total_amount'] 56 | } 57 | AfdWebhook.save() 58 | # 添加vip用户 59 | await add_vip(order_id['uid'],order_id['day']) 60 | else: 61 | _log.warning(f"[afd] no custom_order_id in afd webhook") 62 | 63 | # 将订单编号中间部分改为# 64 | trno = params['data']['order']['out_trade_no'] 65 | trno_f = trno[0:8] 66 | trno_b = trno[-4:] 67 | trno_f += "####" 68 | trno_f += trno_b 69 | # 构造卡片 70 | cm = CardMessage() 71 | c = Card(Module.Header(f"爱发电有新动态啦!"), Module.Context(Element.Text(f"订单号: {trno_f}")), Module.Divider(), 72 | Module.Section(Element.Text(text, Types.Text.KMD))) 73 | cm.append(c) 74 | _log.debug(json.dumps(cm)) 75 | # 发送到指定频道 76 | debug_ch = await bot.client.fetch_public_channel(config['channel']['debug_ch']) 77 | await bot.client.send(debug_ch, cm) 78 | _log.info(f"trno:{params['data']['order']['out_trade_no']} | afd-cm-send") 79 | # 返回状态码 80 | return {"ec": 200, "em": "success"} 81 | -------------------------------------------------------------------------------- /utils/file/fileManage.py: -------------------------------------------------------------------------------- 1 | import json 2 | import aiofiles 3 | from ..myLog import _log 4 | 5 | FileList = [] 6 | """files need to write into storage""" 7 | 8 | def open_file(path): 9 | with open(path, 'r', encoding='utf-8') as f: 10 | tmp = json.load(f) 11 | return tmp 12 | 13 | 14 | async def write_file_aio(path: str, value): 15 | async with aiofiles.open(path, 'w', encoding='utf-8') as f: 16 | await f.write(json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False)) 17 | 18 | 19 | def write_file(path: str, value): 20 | with open(path, 'w', encoding='utf-8') as fw2: 21 | json.dump(value, fw2, indent=2, sort_keys=True, ensure_ascii=False) 22 | 23 | 24 | async def save_all_file(is_Aio=True): 25 | """save all file in FileList 26 | """ 27 | for i in FileList: 28 | try: 29 | if is_Aio: 30 | await i.save_aio() 31 | else: 32 | i.save() 33 | except: 34 | _log.exception(f"Save.All.File | {i.path}") 35 | 36 | _log.info(f"Save.All.File | save finished") 37 | 38 | 39 | # 文件管理类 40 | class FileManage: 41 | # 初始化构造 42 | def __init__(self, path: str, read_only: bool = False) -> None: 43 | with open(path, 'r', encoding='utf-8') as f: 44 | tmp = json.load(f) 45 | self.value = tmp # 值 46 | self.type = type(tmp) # 值的类型 47 | self.path = path # 值的文件路径 48 | self.Ronly = read_only # 是否只读 49 | #将自己存全局变量里面 50 | if not read_only: 51 | global FileList # 如果不是只读,那就存list里面 52 | FileList.append(self) 53 | 54 | # []操作符重载 55 | def __getitem__(self, index): 56 | return self.value[index] 57 | 58 | # 打印重载 59 | def __str__(self) -> str: 60 | return str(self.value) 61 | 62 | # 删除成员 63 | def __delitem__(self, index): 64 | del self.value[index] 65 | 66 | # 长度 67 | def __len__(self): 68 | return len(self.value) 69 | 70 | # 索引赋值 x[i] = 1 71 | def __setitem__(self, index, value): 72 | self.value[index] = value 73 | 74 | # 迭代 75 | def __iter__(self): 76 | return self.value.__iter__() 77 | 78 | def __next__(self): 79 | return self.value.__next__() 80 | 81 | # 比较== 82 | def __eq__(self, i): 83 | if isinstance(i, FileManage): 84 | return self.value.__eq__(i.value) 85 | else: 86 | return self.value.__eq__(i) 87 | 88 | # 比较!= 89 | def __ne__(self, i): 90 | if isinstance(i, FileManage): 91 | return self.value.__ne__(i.value) 92 | else: 93 | return self.value.__ne__(i) 94 | 95 | # 获取成员 96 | def get_instance(self): 97 | return self.value 98 | 99 | # 遍历dict 100 | def items(self): 101 | return self.value.items() 102 | 103 | # 追加 104 | def append(self, i): 105 | self.value.append(i) 106 | 107 | # list的删除 108 | def remove(self, i): 109 | self.value.remove(i) 110 | 111 | def keys(self): 112 | return self.value.keys() 113 | 114 | # 保存 115 | def save(self): 116 | with open(self.path, 'w', encoding='utf-8') as fw: 117 | json.dump(self.value, fw, indent=2, sort_keys=True, ensure_ascii=False) 118 | 119 | # 异步保存 120 | async def save_aio(self): 121 | async with aiofiles.open(self.path, 'w', encoding='utf-8') as f: #这里必须用dumps 122 | await f.write(json.dumps(self.value, indent=2, sort_keys=True, ensure_ascii=False)) -------------------------------------------------------------------------------- /utils/botVip.py: -------------------------------------------------------------------------------- 1 | import time 2 | import copy 3 | from typing import Union 4 | from datetime import datetime,timedelta,timezone 5 | from khl import Bot,Message 6 | from khl.card import CardMessage,Card,Module,Element,Types 7 | 8 | from .file.files import bot,VipUserDict,VipUser,_log 9 | 10 | DAY_TIMES = 86400 11 | """24H in secons""" 12 | 13 | def vip_time_remain(user_id)->float: 14 | """ 15 | get time remain of vip, return in seconds 16 | """ 17 | # 时间差值 18 | timeout = VipUserDict[user_id]['time'] - time.time() 19 | return timeout 20 | 21 | async def vip_time_remain_cm(times): 22 | """获取vip时间剩余卡片消息""" 23 | cm = CardMessage() 24 | c1 = Card(color='#e17f89') 25 | c1.append(Module.Section(Element.Text('您的「vip会员」还剩', Types.Text.KMD))) 26 | c1.append(Module.Divider()) 27 | c1.append(Module.Countdown(datetime.now() + timedelta(seconds=times), mode=Types.CountdownMode.DAY)) 28 | cm.append(c1) 29 | return cm 30 | 31 | def get_none_vip_cm(): 32 | """card msg info user not vip""" 33 | c = Card(color='#e17f89') 34 | c.append(Module.Section(Element.Text("您并非vip用户", Types.Text.KMD))) 35 | cm = CardMessage(c) 36 | return cm 37 | 38 | # 检查用户vip是否失效或者不是vip 39 | async def vip_ck(msg:Union[Message,str])->bool: 40 | """ 41 | params can be: 42 | * `msg:Message` check & inform user if they aren't vip 43 | * `author_id:str` will not send reply, just check_if_vip 44 | 45 | retuns: 46 | * True: is vip 47 | * False: not vip 48 | """ 49 | # 判断是否为msg对象并获取用户id 50 | is_msg = isinstance(msg, Message) 51 | user_id = msg if not is_msg else msg.author_id 52 | cm = CardMessage() if not is_msg else get_none_vip_cm() 53 | 54 | # 检查 55 | if user_id in VipUserDict: 56 | # 用户的vip是否过期? 57 | if time.time() > VipUserDict[user_id]['time']: 58 | del VipUserDict[user_id] 59 | # 如果是消息,那就发送提示 60 | if is_msg: 61 | await msg.reply(cm) 62 | _log.info(f"[vip-ck] Au:{user_id} msg.reply | vip out of date") 63 | return False 64 | else: #没有过期,返回真 65 | _log.info(f"[vip-ck] Au:{user_id} is vip") 66 | return True 67 | # 用户不是vip 68 | else: 69 | if is_msg: #如果是消息,那就发送提示 70 | await msg.reply(cm) 71 | _log.info(f"[vip-ck] Au:{user_id} msg.reply | not vip") 72 | return False 73 | 74 | 75 | async def add_vip(user_id:str,day:int,user_name=""): 76 | """ 77 | - 提供用户id和天数,新增vip用户 78 | - 如果用户已存在,则添加vip时长 79 | """ 80 | global VipUserDict 81 | if user_id not in VipUserDict: 82 | VipUserDict[user_id] ={"name":user_name,"time":time.time()} 83 | if not user_name: 84 | user = await bot.client.fetch_user(user_id) 85 | VipUserDict[user_id]["name"] = f"{user.username}#{user.identify_num}" 86 | # time字段是初始化vip时间+剩余时间,也就是vip时间截止的时间戳 87 | VipUserDict[user_id]["time"] += day * DAY_TIMES 88 | _log.info(f"[vip-add] Au:{user_id} | day:{day} | time:{VipUserDict[user_id]['time']}") 89 | 90 | 91 | async def fetch_vip_user(): 92 | """获取当前vip用户列表(这会剔除过期vip)""" 93 | global VipUserDict,VipUser 94 | vipuserdict_temp = copy.deepcopy(VipUserDict) 95 | text = "" 96 | for u, ifo in vipuserdict_temp.items(): 97 | if await vip_ck(u): # vip-ck会主动修改dict 98 | time = vip_time_remain(u) 99 | time = format(time / DAY_TIMES, '.2f') 100 | # 通过/86400计算出大概的天数 101 | text += f"{u}_{ifo['name']}\t = {time}\n" 102 | 103 | if vipuserdict_temp != VipUserDict: 104 | #将修改存放到文件中 105 | VipUser.save() 106 | _log.info(f"[vip-r] update VipUserDict") 107 | 108 | return text -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import traceback 4 | import aiohttp 5 | 6 | from khl import Bot,Message,Channel 7 | from khl.card import CardMessage,Card,Module,Element,Types 8 | 9 | from utils.gTime import getTime 10 | from utils.file.files import bot,AfdWebhook,VipUserDict,config 11 | from utils.file.fileManage import save_all_file 12 | from utils.myLog import _log,logMsg 13 | from utils import botVip 14 | 15 | debug_ch:Channel 16 | """日志频道""" 17 | kook_base_url = "https://www.kookapp.cn" 18 | kook_headers = {f'Authorization': f"Bot {config['bot']['token']}"} 19 | 20 | async def bot_offline(): 21 | """下线机器人""" 22 | url = kook_base_url + "/api/v3/user/offline" 23 | async with aiohttp.ClientSession() as session: 24 | async with session.post(url, headers=kook_headers) as response: 25 | res = json.loads(await response.text()) 26 | _log.debug(res) 27 | return res 28 | 29 | 30 | # 每5分钟保存一次文件 31 | @bot.task.add_interval(minutes=5) 32 | async def save_file_task(): 33 | try: 34 | await save_all_file() 35 | except: 36 | err_cur = f"ERR! [{getTime()}] [Save.File.Task]\n```\n{traceback.format_exc()}\n```" 37 | _log.exception("ERR in Save_File_Task") 38 | await bot.client.send(debug_ch, err_cur) # type: ignore 39 | 40 | # 看看机器人活着不 41 | @bot.command(name='alive',case_sensitive=False) 42 | async def alive_cmd(msg:Message,*arg): 43 | logMsg(msg) 44 | await msg.reply(f"bot alive here") 45 | 46 | # 获取vip剩余时间 47 | @bot.command(name='vip',case_sensitive=False) 48 | async def vip_cmd(msg:Message,*arg): 49 | logMsg(msg) 50 | try: 51 | if not await botVip.vip_ck(msg): 52 | return 53 | time_remain = botVip.vip_time_remain(msg.author_id) 54 | cm = await botVip.vip_time_remain_cm(time_remain) 55 | await msg.reply(cm) 56 | except: 57 | _log.exception(f"Err in vip") 58 | 59 | # 获取vip列表 60 | @bot.command(name='vip-l',case_sensitive=False) 61 | async def vip_list_cmd(msg:Message,*arg): 62 | logMsg(msg) 63 | try: 64 | text = await botVip.fetch_vip_user() 65 | if not text: 66 | text="当前没有vip用户" 67 | cm = CardMessage(Card(Module.Section(Element.Text(text,Types.Text.KMD)))) 68 | await msg.reply(cm) 69 | except: 70 | _log.exception(f"Err in vip-l") 71 | 72 | # 测试你是否为vip 73 | @bot.command(name='vip-test',case_sensitive=False) 74 | async def vip_test_cmd(msg:Message,*arg): 75 | logMsg(msg) 76 | try: 77 | if await botVip.vip_ck(msg): 78 | cm = CardMessage(Card(Module.Section(Element.Text("您是vip!",Types.Text.KMD)))) 79 | await msg.reply(cm) 80 | except: 81 | _log.exception(f"Err in vip-test") 82 | 83 | # 商店 84 | @bot.command(name='shop',case_sensitive=False) 85 | async def shop_cmd(msg:Message,*arg): 86 | logMsg(msg) 87 | try: 88 | cm = CardMessage() 89 | c =Card(Module.Section(Element.Text("欢迎选购机器人Vip",Types.Text.KMD))) 90 | # ------------- 91 | # vip商品1,周vip 92 | vip_item_link1 = "https://afdian.net/order/create?product_type=1&plan_id=9aea871c304911ed8ec452540025c377&sku=%5B%7B%22sku_id%22%3A%229aed6edc304911edbeb552540025c377%22,%22count%22%3A1%7D%5D" 93 | # 添加上自定义订单号的字符串 94 | vip_item_link1+= f"&custom_order_id={msg.author_id}:7" 95 | c.append( 96 | Module.Section( 97 | Element.Text("周vip", Types.Text.KMD), 98 | Element.Button("购买", vip_item_link1, Types.Click.LINK))) 99 | # ------------- 100 | 101 | # vip商品2,月vip 102 | vip_item_link2 = "https://afdian.net/order/create?product_type=1&plan_id=ff2949022e9611ed89d452540025c377&sku=%5B%7B%22sku_id%22%3A%22ff2bb4f82e9611ed83ac52540025c377%22,%22count%22%3A1%7D%5D" 103 | # 添加上自定义订单号的字符串 104 | vip_item_link2+= f"&custom_order_id={msg.author_id}:30" 105 | c.append( 106 | Module.Section( 107 | Element.Text("月vip", Types.Text.KMD), 108 | Element.Button("购买", vip_item_link2, Types.Click.LINK))) 109 | 110 | cm.append(c) 111 | await msg.reply(cm,is_temp=True) # 临时消息,所以这个按钮只有当前用户可以点 112 | except: 113 | _log.exception(f"Err in shop") 114 | 115 | 116 | @bot.command(name='kill') 117 | async def KillBot(msg: Message,at_text="", *arg): 118 | logMsg(msg) 119 | try: 120 | if not at_text: 121 | return await msg.reply(f"必须at机器人才能下线 `/kill @机器人`") 122 | 123 | # 保存所有文件 124 | await save_all_file(False) 125 | await msg.reply(f"[KILL] 保存全局变量成功,bot下线") 126 | res = "webhook" 127 | if config['bot']['ws']: 128 | res = await bot_offline() # 调用接口下线bot 129 | _log.info(f"KILL | bot-off: {res}\n") 130 | os._exit(0) # 退出程序 131 | except: 132 | _log.exception(f"Err in kill") 133 | 134 | 135 | @bot.on_startup 136 | async def startup_task(bot:Bot): 137 | """启动任务""" 138 | try: 139 | global debug_ch 140 | debug_ch = await bot.client.fetch_public_channel(config['channel']['debug_ch']) 141 | _log.info(f"[BOT.START] fetch channel success") 142 | except: 143 | _log.exception(f"[BOT.START] Err") 144 | os.abort() 145 | 146 | if __name__ == '__main__': 147 | _log.info(f"[BOT] start at {getTime()}") 148 | bot.run() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Kook-Afd-Webhook-Bot

2 | 3 |
4 | 5 | 6 | ![python](https://img.shields.io/badge/Python-3.10-green) ![commit](https://img.shields.io/github/last-commit/musnows/Kook-Afd-Webhook-Bot) 7 | [![khl server](https://www.kaiheila.cn/api/v3/badge/guild?guild_id=3986996654014459&style=0)](https://kook.top/gpbTwZ) ![githubstars](https://img.shields.io/github/stars/musnows/Kook-Afd-Webhook-Bot?style=social) 8 | 9 |
10 | 11 | 12 | ## 1.简介 13 | 14 | 本文档主要讲解如何将kook机器人与爱发电的webhook功能进行对接 15 | 16 | > 商品以vip作为示例,提供一个基础的python-demo 17 | 18 | 主要流程如下 19 | 20 | * 机器人提供vip商城命令,用户执行,机器人发送一张包含不同价位vip价格和购买url的卡片给用户(临时消息,只有这个用户能看到) 21 | * 用户购买商品时,爱发电发送webhook给机器人 22 | * 机器人获取webhook信息中的`custom_order_id`,解析出用户id和购买天数,自动给对应用户上vip 23 | 24 | 相关文档 25 | 26 | * 机器人所用框架:[github.com/TWT233/khl.py](https://github.com/TWT233/khl.py) 27 | * 爱发电webhook接口文档:https://afdian.net/p/9c65d9cc617011ed81c352540025c377 28 | 29 | 开始前,你需要先拥有自己的[kook-bot](https://developer.kookapp.cn/doc/intro),并注册[爱发电](https://afdian.net/),申请爱发电开发者权限 30 | 31 | > 如果你想使用CDK(即兑换码)的方式来分发vip,可以看看我的 [Valorant-Shop-CN/Kook-Valorant-Bot](https://github.com/Valorant-Shop-CN/Kook-Valorant-Bot) 项目 32 | 33 | ## 2.python-demo 34 | 35 | ### 2.1 webhook-api 36 | 37 | 要想和爱发电的api对接,首先bot自己需要维护一个webhook的api-url。这也要求你的机器人是部署在**可公网访问**的环境中,否则爱发电的webhook无法送达。 38 | 39 | * 爱发电开发者页面 https://afdian.net/dashboard/dev 40 | * 该url**建议**为https协议,如果使用https,你还需要绑定域名并配置ssl 41 | 42 | 同时,为了保证webhook中始终有`custom_order_id`字段,您需要告知您的用户,只能通过机器人生成的url来购买vip。否则自动化流程将失效 43 | 44 | * 机器人收到爱发电webhook 45 | * 解析webhook中的键值,获取到`custom_order_id` 46 | * 解析`custom_order_id`,获取到kook用户id和vip天数 47 | * 给用户添加上vip天数 48 | 49 | ~~~python 50 | custom_order_id=kook用户id:vip天数 51 | ~~~ 52 | 53 | 处理代码详见 [api.py](./api.py) 和 [apiHandler.py](./utils/apiHandler.py) 54 | 55 | ### 2.2 机器人 56 | 57 | 机器人命令如下 58 | 59 | | 命令 | 说明 | 60 | | --------- | ---------------------------------------------- | 61 | | /shop | 获取购买vip的卡片 | 62 | | /vip | 看看自己vip剩余时长 | 63 | | /vip-l | 获取vip用户列表(会刷掉过期的vip用户) | 64 | | /vip-test | 该命令只有vip用户才能执行,用于测试vip是否生效 | 65 | | /alive | 看看机器人活着不 | 66 | | /kill | 机器人下线,并保存文件 | 67 | 68 | 代码详见 [main.py](./main.py) 69 | 70 | #### 2.2.1 vip物品url获取 71 | 72 | 先创建你的vip店铺。爱发电的商品有`隐藏`功能,隐藏后的商品将不会显示在主页上,这样也能实现用户只能通过bot提供的链接来访问购买的操作 73 | 74 | ![image-20230419223059244](./img/643ffb22e93a1.png) 75 | 76 | ![image-20230419223112664](./img/643ffb303e7c4.png) 77 | 78 | 这里我拿周vip和月vip作为示例 79 | 80 | ![643fce843df92](./img/643fce843df92.png) 81 | 82 | 点击商品,进入详情页,点击发电 83 | 84 | ![643fceb15227a](./img/643fceb15227a.png) 85 | 86 | 进入付款页面后,复制最上方的url 87 | 88 | ![image-20230419192145330](./img/643fcec9808af.png) 89 | 90 | ~~~ 91 | https://afdian.net/order/create?product_type=1&plan_id=9aea871c304911ed8ec452540025c377&sku=%5B%7B%22sku_id%22%3A%229aed6edc304911edbeb552540025c377%22,%22count%22%3A1%7D%5D 92 | ~~~ 93 | 94 | 我们要做的就是在这个url尾部添加上`custom_order_id` 95 | 96 | ~~~ 97 | &custom_order_id=kook用户id:vip天数 98 | ~~~ 99 | 100 | 添加完毕后的链接如下 101 | 102 | ~~~ 103 | https://afdian.net/order/create?product_type=1&plan_id=9aea871c304911ed8ec452540025c377&sku=%5B%7B%22sku_id%22%3A%229aed6edc304911edbeb552540025c377%22,%22count%22%3A1%7D%5D&custom_order_id=kook用户id:vip天数 104 | ~~~ 105 | 106 | 复制到浏览器,仍可正常访问,代表配置无误 107 | 108 | ![image-20230419192256714](./img/643fcf10cb956.png) 109 | 110 | #### 2.2.2 vip物品url配置 111 | 112 | 找到`main.py`中的如下代码,将里面的`vip_item_link`替换成你自己的url。如果需要添加更多商品,将两个`----------`中间的部分多复制几份即可 113 | 114 | ~~~python 115 | @bot.command(name='shop',case_sensitive=False) 116 | async def shop_cmd(msg:Message,*arg): 117 | logMsg(msg) 118 | try: 119 | cm = CardMessage() 120 | c =Card(Module.Section(Element.Text("欢迎选购机器人Vip",Types.Text.KMD))) 121 | ## ------------- 122 | ## vip商品1,周vip 123 | vip_item_link1 = "https://afdian.net/order/create?product_type=1&plan_id=9aea871c304911ed8ec452540025c377&sku=%5B%7B%22sku_id%22%3A%229aed6edc304911edbeb552540025c377%22,%22count%22%3A1%7D%5D" 124 | ## 添加上自定义订单号的字符串 125 | vip_item_link1+= f"&custom_order_id={msg.author_id}:7" 126 | c.append( 127 | Module.Section( 128 | Element.Text("周vip", Types.Text.KMD), 129 | Element.Button("购买", vip_item_link1, Types.Click.LINK))) 130 | ## ------------- 131 | 132 | ## vip商品2,月vip 133 | vip_item_link2 = "https://afdian.net/order/create?product_type=1&plan_id=ff2949022e9611ed89d452540025c377&sku=%5B%7B%22sku_id%22%3A%22ff2bb4f82e9611ed83ac52540025c377%22,%22count%22%3A1%7D%5D" 134 | ## 添加上自定义订单号的字符串 135 | vip_item_link2+= f"&custom_order_id={msg.author_id}:30" 136 | c.append( 137 | Module.Section( 138 | Element.Text("月vip", Types.Text.KMD), 139 | Element.Button("购买", vip_item_link2, Types.Click.LINK))) 140 | 141 | cm.append(c) 142 | await msg.reply(cm,is_temp=True) ## 临时消息,所以这个按钮只有当前用户可以点 143 | except: 144 | _log.exception(f"Err in shop") 145 | ~~~ 146 | 147 | #### 2.2.3 启动机器人并配置webhook 148 | 149 | 先安装依赖项(Python版本3.10) 150 | 151 | ~~~ 152 | pip3.10 install -r requierments.txt 153 | ~~~ 154 | 155 | * 配置文件示例`config/config.exp.json` 156 | * 在内部填写正确的机器人token字段后,重命名为`config.json` 157 | * 并将`config/log.exp`中的两个文件复制到`log/`路径下 158 | 159 | 然后启动机器人 160 | 161 | ~~~ 162 | python3.10 start.py 163 | ~~~ 164 | 165 | 看到如下输出即为启动成功 166 | 167 | ![image-20230419193628380](./img/643fd23c6dff9.png) 168 | 169 | 我们需要将api的地址填写到爱发电的webhook url中 170 | 171 | * 开发者页面 https://afdian.net/dashboard/dev 172 | 173 | 记得开放对应端口防火墙,并正确绑定域名和开启https 174 | 175 | ![image-20230419194646676](./img/643fd4a6a408b.png) 176 | 177 | 填写url后点击保存,爱发电会发送一条测试webhook给你的机器人。如果在预先定义的debug_ch中看到了如下卡片,则代表webhook配置成功 178 | 179 | ![image-20230419194631552](./img/643fd4979854c.png) 180 | 181 | ~~~ 182 | [23-04-19 19:46:07] INFO:api.py:aifadian_webhook:31 | request | /afd 183 | [23-04-19 19:46:07] INFO:apiHandler.py:afd_request:71 | trno:202106232138371083454010626 | afd-cm-send 184 | ~~~ 185 | 186 | kill掉机器人后,在log文件中也能看到这次测试webhook的请求体 187 | 188 | ![image-20230419194914026](./img/643fd53a4804f.png) 189 | 190 | ### 2.3 命令截图 191 | 192 | #### 2.3.1 基础测试 193 | 194 | 先测试一下机器人上线没有 195 | 196 | ![image-20230419193407783](./img/643fd1afc1a49.png) 197 | 198 | 刚开始时,没有vip用户 199 | 200 | ![image-20230419193337782](./img/643fd191e4690.png) 201 | 202 | ![image-20230419193356371](./img/643fd1a45f307.png) 203 | 204 | 使用商城命令,获取购买按钮 205 | 206 | ![image-20230419194053131](./img/643fd345357d4.png) 207 | 208 | 点击按钮,会跳转到爱发电的付款页面,能看到url最后成功附着上了用户id和时间 209 | 210 | ~~~ 211 | &custom_order_id=1961572535%3A7 212 | ~~~ 213 | 214 | url中的`%3A`就是`:` 215 | 216 | ---- 217 | 218 | #### 2.3.2 购入vip测试 219 | 220 | 如下,我购买了一个周vip,机器人成功获取到了webhook体中的自定义订单id 221 | 222 | ![image-20230419195247714](./img/643fd60fbabe7.png) 223 | 224 | 此时再次执行vip命令,能看到已经正确添加上了7天的vip 225 | 226 | ![image-20230419195407303](./img/643fd65f46258.png) 227 | 228 | ![image-20230419195631005](./img/643fd6ef0ec7e.png) 229 | 230 | 日志文件也成功记录 231 | 232 | ![image-20230419195714087](./img/643fd71a104c1.png) 233 | 234 | ![image-20230419195649205](./img/643fd7012ccc0.png) 235 | 236 | 测试完毕! 237 | 238 | ## The end 239 | 240 | 有任何问题,都可以加入我的[帮助服务器](https://kook.top/gpbTwZ)与我联系 241 | 242 | [![khl server](https://www.kaiheila.cn/api/v3/badge/guild?guild_id=3986996654014459&style=0)](https://kook.top/gpbTwZ) 243 | --------------------------------------------------------------------------------