2022-06-17 05:59:15 +00:00
|
|
|
|
import asyncio
|
|
|
|
|
import random
|
2022-06-19 06:36:25 +00:00
|
|
|
|
import traceback
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
2022-06-19 06:12:41 +00:00
|
|
|
|
from bs4 import BeautifulSoup
|
2022-06-18 06:03:40 +00:00
|
|
|
|
from sqlalchemy import create_engine, Column, String, Text, Integer
|
2022-06-17 05:59:15 +00:00
|
|
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
|
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
|
|
|
|
from tenacity import retry, stop_after_attempt
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
from core.component import on_command
|
|
|
|
|
from core.elements import MessageSession, Image, Plain
|
2022-06-19 06:58:43 +00:00
|
|
|
|
from core.utils import get_url, download_to_cache, random_cache_path
|
2022-06-17 06:35:59 +00:00
|
|
|
|
from core.logger import Logger
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
2022-06-19 06:58:43 +00:00
|
|
|
|
from PIL import Image as PILImage
|
|
|
|
|
|
2022-06-18 05:18:45 +00:00
|
|
|
|
|
2022-06-17 05:59:15 +00:00
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
DB_LINK = 'sqlite:///modules/chemical_code/answer.db' # 题型数据库
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
class Answer(Base): # 数据表,为了使用 sqlalchemy 进行数据库操作所以预设此类
|
2022-06-17 05:59:15 +00:00
|
|
|
|
__tablename__ = "Answer"
|
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
|
|
|
cas = Column(String(512))
|
|
|
|
|
answer = Column(Text)
|
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
class MSGDBSession: # 数据库会话类
|
2022-06-17 05:59:15 +00:00
|
|
|
|
def __init__(self):
|
|
|
|
|
self.engine = engine = create_engine(DB_LINK)
|
|
|
|
|
Base.metadata.create_all(bind=engine, checkfirst=True)
|
|
|
|
|
self.Session = sessionmaker()
|
|
|
|
|
self.Session.configure(bind=self.engine)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def session(self):
|
|
|
|
|
return self.Session()
|
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
session = MSGDBSession().session # 实例化数据库会话并获取数据库会话
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
def auto_rollback_error(func): # 函数装饰器,用于捕获异常并回滚数据库
|
2022-06-17 05:59:15 +00:00
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
try:
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
@retry(stop=stop_after_attempt(3)) # 函数装饰器,用于重试 3 次
|
|
|
|
|
@auto_rollback_error # 函数装饰器,用于捕获异常并回滚数据库
|
|
|
|
|
def randcc(): # 随机从数据库(数据来源:chemicalbook)中获取一个题目
|
|
|
|
|
num = random.randint(1, 20000) # 在 1 到 20000 之间随机一个数作为抽取 ID
|
|
|
|
|
query = session.query(Answer).filter_by(id=num).first() # 根据 ID 查询题目
|
|
|
|
|
return query.cas, query.answer # 返回 chemicalbook 中的 CAS 号和化学式
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
csr_link = 'https://www.chemspider.com' # ChemSpider 的链接
|
2022-06-19 06:12:41 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 06:36:25 +00:00
|
|
|
|
@retry(stop=stop_after_attempt(3), reraise=True)
|
2022-06-19 10:02:51 +00:00
|
|
|
|
async def search_csr(id=None): # 根据 ChemSpider 的 ID 查询 ChemSpider 的链接,留空(将会使用缺省值 None)则随机查询
|
|
|
|
|
if id is not None: # 如果传入了 ID,则使用 ID 查询
|
2022-06-19 08:06:11 +00:00
|
|
|
|
answer = id
|
|
|
|
|
else:
|
2022-06-19 10:02:51 +00:00
|
|
|
|
cas, answer = randcc() # 否则随机查询一个题目
|
|
|
|
|
get = await get_url(csr_link + '/Search.aspx?q=' + answer, 200, fmt='text') # 在 ChemSpider 上搜索此化学式或 ID
|
2022-06-19 06:36:25 +00:00
|
|
|
|
# Logger.info(get)
|
2022-06-19 10:02:51 +00:00
|
|
|
|
soup = BeautifulSoup(get, 'html.parser') # 解析 HTML
|
|
|
|
|
rlist = [] # 创建一个空列表用于存放搜索结果
|
|
|
|
|
try: # 尝试获取搜索结果
|
|
|
|
|
results = soup.find_all('tbody')[0].find_all('tr') # 获取搜索结果中的所有表格行
|
|
|
|
|
for x in results: # 遍历所有表格行
|
|
|
|
|
sub = x.find_all('td')[0:4] # 获取表格行中的前四个单元格
|
|
|
|
|
name = sub[2].text # 单元格中的化学式名称
|
|
|
|
|
image = sub[1].find_all('img')[0].get('src') # 单元格中的图片链接
|
|
|
|
|
rlist.append({'name': name, 'image': csr_link + image + '&w=500&h=500'}) # 将化学式名称和图片链接加入列表
|
|
|
|
|
except IndexError: # 尝试失败,进行第二次尝试
|
2022-06-19 08:06:11 +00:00
|
|
|
|
try:
|
|
|
|
|
name = soup.find('span',
|
2022-06-19 10:02:51 +00:00
|
|
|
|
id='ctl00_ctl00_ContentSection_ContentPlaceHolder1_RecordViewDetails_rptDetailsView_ctl00_prop_MF').text # 获取化学式名称
|
2022-06-19 08:06:11 +00:00
|
|
|
|
image = soup.find('img',
|
|
|
|
|
id='ctl00_ctl00_ContentSection_ContentPlaceHolder1_RecordViewDetails_rptDetailsView_ctl00_ThumbnailControl1_viewMolecule')\
|
2022-06-19 10:02:51 +00:00
|
|
|
|
.get('src') # 获取图片链接
|
2022-06-19 08:06:11 +00:00
|
|
|
|
rlist.append({'name': name, 'image': csr_link + image})
|
2022-06-19 10:02:51 +00:00
|
|
|
|
except Exception as e: # 尝试失败,抛出错误
|
2022-06-19 08:06:11 +00:00
|
|
|
|
raise e
|
2022-06-19 06:12:41 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 06:36:25 +00:00
|
|
|
|
return rlist
|
2022-06-19 06:12:41 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-17 12:44:04 +00:00
|
|
|
|
cc = on_command('chemical_code', alias=['cc', 'chemicalcode'], desc='化学式验证码测试', developers=['OasisAkari'])
|
2022-06-19 10:02:51 +00:00
|
|
|
|
play_state = {} # 创建一个空字典用于存放游戏状态
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
@cc.handle() # 直接使用 cc 命令将触发此装饰器
|
2022-06-19 08:06:11 +00:00
|
|
|
|
async def chemical_code_by_random(msg: MessageSession):
|
2022-06-19 10:02:51 +00:00
|
|
|
|
await c(msg) # 将消息会话传入 c 函数
|
2022-06-19 08:06:11 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 08:15:14 +00:00
|
|
|
|
@cc.handle('stop {停止当前的游戏。}')
|
|
|
|
|
async def s(msg: MessageSession):
|
2022-06-19 10:02:51 +00:00
|
|
|
|
state = play_state.get(msg.target.targetId, False) # 尝试获取 play_state 中是否有此对象的游戏状态
|
|
|
|
|
if state: # 若有
|
|
|
|
|
if state['active']: # 检查是否为活跃状态
|
|
|
|
|
play_state[msg.target.targetId]['active'] = False # 标记为非活跃状态
|
|
|
|
|
await msg.sendMessage(f'已停止,正确答案是 {state["answer"]}', quote=False) # 发送存储于 play_state 中的答案
|
2022-06-19 08:15:14 +00:00
|
|
|
|
else:
|
|
|
|
|
await msg.sendMessage('当前无活跃状态的游戏。')
|
|
|
|
|
else:
|
|
|
|
|
await msg.sendMessage('当前无活跃状态的游戏。')
|
|
|
|
|
|
|
|
|
|
|
2022-06-19 09:11:36 +00:00
|
|
|
|
@cc.handle('<chemspider id> {根据 ChemSpider ID 出题}')
|
2022-06-19 08:06:11 +00:00
|
|
|
|
async def chemical_code_by_id(msg: MessageSession):
|
2022-06-19 10:02:51 +00:00
|
|
|
|
id = msg.parsed_msg['<chemspider id>'] # 从已解析的消息中获取 ChemSpider ID
|
|
|
|
|
if id.isdigit(): # 如果 ID 为纯数字
|
|
|
|
|
await c(msg, id) # 将消息会话和 ID 一并传入 c 函数
|
2022-06-19 08:09:52 +00:00
|
|
|
|
else:
|
2022-06-19 08:11:03 +00:00
|
|
|
|
await msg.finish('请输入纯数字ID!')
|
2022-06-19 08:06:11 +00:00
|
|
|
|
|
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
async def c(msg: MessageSession, id=None): # 要求传入消息会话和 ChemSpider ID,ID 留空将会使用缺省值 None
|
|
|
|
|
if msg.target.targetId in play_state and play_state[msg.target.targetId]['active']: # 检查对象(群组或私聊)是否在 play_state 中有记录及是否为活跃状态
|
2022-06-17 05:59:15 +00:00
|
|
|
|
await msg.finish('当前有一局游戏正在进行中。')
|
2022-06-19 10:02:51 +00:00
|
|
|
|
play_state.update({msg.target.targetId: {'active': True}}) # 若无,则创建一个新的记录并标记为活跃状态
|
2022-06-19 06:36:25 +00:00
|
|
|
|
try:
|
2022-06-19 10:02:51 +00:00
|
|
|
|
csr = await search_csr(id) # 尝试获取 ChemSpider ID 对应的化学式列表
|
|
|
|
|
except Exception as e: # 意外情况
|
|
|
|
|
traceback.print_exc() # 打印错误信息
|
|
|
|
|
play_state[msg.target.targetId]['active'] = False # 将对象标记为非活跃状态
|
2022-06-19 06:36:25 +00:00
|
|
|
|
return await msg.finish('发生错误:拉取题目失败,请重新发起游戏。')
|
2022-06-19 06:58:43 +00:00
|
|
|
|
# print(csr)
|
2022-06-19 10:02:51 +00:00
|
|
|
|
choice = random.choice(csr) # 从列表中随机选择一个结果
|
|
|
|
|
play_state[msg.target.targetId]['answer'] = choice['name'] # 将正确答案标记于 play_state 中存储的对象中
|
|
|
|
|
Logger.info(f'Answer: {choice["name"]}') # 在日志中输出正确答案
|
2022-06-19 06:12:41 +00:00
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
# 下面的代码已被注释,因为不再使用 ChemicalBook 图片数据源
|
2022-06-19 06:12:41 +00:00
|
|
|
|
"""get_image = await download_to_cache(f'https://www.chemicalbook.com/CAS/GIF/{get_rand[0]}.gif')
|
2022-06-17 06:35:59 +00:00
|
|
|
|
Logger.info(get_rand[1])
|
2022-06-18 06:03:40 +00:00
|
|
|
|
play_state[msg.target.targetId]['answer'] = get_rand[1]
|
2022-06-18 05:18:45 +00:00
|
|
|
|
|
|
|
|
|
with PILImage.open(get_image) as im:
|
2022-06-19 04:45:52 +00:00
|
|
|
|
if im.size[0] < 10:
|
2022-06-18 06:03:40 +00:00
|
|
|
|
del play_state[msg.target.targetId]
|
2022-06-18 05:18:45 +00:00
|
|
|
|
return await _(msg)
|
|
|
|
|
im.seek(0)
|
|
|
|
|
image = im.convert("RGBA")
|
|
|
|
|
datas = image.getdata()
|
|
|
|
|
newData = []
|
|
|
|
|
for item in datas:
|
|
|
|
|
if item[3] == 0: # if transparent
|
2022-06-19 03:42:32 +00:00
|
|
|
|
newData.append((230, 230, 230)) # set transparent color in jpg
|
2022-06-18 05:18:45 +00:00
|
|
|
|
else:
|
|
|
|
|
newData.append(tuple(item[:3]))
|
2022-06-19 03:28:18 +00:00
|
|
|
|
image = PILImage.new("RGBA", im.size)
|
2022-06-18 05:18:45 +00:00
|
|
|
|
image.getdata()
|
|
|
|
|
image.putdata(newData)
|
2022-06-19 03:28:18 +00:00
|
|
|
|
newpath = random_cache_path() + '.png'
|
2022-06-19 06:12:41 +00:00
|
|
|
|
image.save(newpath)"""
|
2022-06-18 05:18:45 +00:00
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
download = await download_to_cache(choice['image']) # 从结果中获取链接并下载图片
|
2022-06-19 06:58:43 +00:00
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
with PILImage.open(download) as im: # 打开下载的图片
|
|
|
|
|
datas = im.getdata() # 获取图片数组
|
2022-06-19 06:58:43 +00:00
|
|
|
|
newData = []
|
2022-06-19 10:02:51 +00:00
|
|
|
|
for item in datas: # 对每个像素点进行处理
|
|
|
|
|
if item[3] == 0: # 如果为透明
|
|
|
|
|
newData.append((230, 230, 230)) # 设置白底
|
2022-06-19 06:58:43 +00:00
|
|
|
|
else:
|
2022-06-19 10:02:51 +00:00
|
|
|
|
newData.append(tuple(item[:3])) # 否则保留原图像素点
|
|
|
|
|
image = PILImage.new("RGBA", im.size) # 创建新图片
|
|
|
|
|
image.getdata() # 获取新图片数组
|
|
|
|
|
image.putdata(newData) # 将处理后的数组覆盖新图片
|
|
|
|
|
newpath = random_cache_path() + '.png' # 创建新文件名
|
|
|
|
|
image.save(newpath) # 保存新图片
|
2022-06-19 06:58:43 +00:00
|
|
|
|
|
|
|
|
|
await msg.sendMessage([Image(newpath),
|
2022-06-18 05:18:45 +00:00
|
|
|
|
Plain('请于2分钟内发送正确答案。(请使用字母表顺序,如:CHBrClF)')])
|
2022-06-19 10:02:51 +00:00
|
|
|
|
time_start = datetime.now().timestamp() # 记录开始时间
|
|
|
|
|
|
|
|
|
|
async def ans(msg: MessageSession, answer): # 定义回答函数的功能
|
|
|
|
|
wait = await msg.waitAnyone() # 等待对象内的任意人回答
|
|
|
|
|
if play_state[msg.target.targetId]['active']: # 检查对象是否为活跃状态
|
|
|
|
|
if wait.asDisplay() != answer: # 如果回答不正确
|
|
|
|
|
Logger.info(f'{wait.asDisplay()} != {answer}') # 输出日志
|
|
|
|
|
return await ans(wait, answer) # 进行下一轮检查
|
2022-06-18 06:03:40 +00:00
|
|
|
|
else:
|
|
|
|
|
await wait.sendMessage('回答正确。')
|
2022-06-19 10:02:51 +00:00
|
|
|
|
play_state[msg.target.targetId]['active'] = False # 将对象标记为非活跃状态
|
2022-06-18 06:03:40 +00:00
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
async def timer(start): # 计时器函数
|
|
|
|
|
if play_state[msg.target.targetId]['active']: # 检查对象是否为活跃状态
|
|
|
|
|
if datetime.now().timestamp() - start > 120: # 如果超过2分钟
|
2022-06-18 06:03:40 +00:00
|
|
|
|
await msg.sendMessage(f'已超时,正确答案是 {play_state[msg.target.targetId]["answer"]}', quote=False)
|
|
|
|
|
play_state[msg.target.targetId]['active'] = False
|
2022-06-19 10:02:51 +00:00
|
|
|
|
else: # 如果未超时
|
|
|
|
|
await asyncio.sleep(1) # 等待1秒
|
|
|
|
|
await timer(start) # 重新调用计时器函数
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|
2022-06-19 10:02:51 +00:00
|
|
|
|
await asyncio.gather(ans(msg, choice['name']), timer(time_start)) # 同时启动回答函数和计时器函数
|
2022-06-17 05:59:15 +00:00
|
|
|
|
|