musume.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. import asyncio
  2. import time
  3. import websockets
  4. import aiohttp
  5. import traceback
  6. import json
  7. import random
  8. import base64
  9. import aiosqlite
  10. import re
  11. import os
  12. wsserver = 'ws://172.17.0.2:3001/'
  13. ws = None
  14. commands = dict ()
  15. trusted_senders = [ 2681243014 ]
  16. confirm_queue = dict ()
  17. private_queue = dict ()
  18. sqlconn = None
  19. GROUP_ID = 1061173220
  20. def wyoj_enable (qq, name):
  21. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  22. raise Exception ('wyoj_enable: Invalid username; possible injection')
  23. os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'U', qq = '{qq}' where username = '{name}'\"")
  24. def wyoj_disable (name):
  25. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  26. raise Exception ('wyoj_disable: Invalid username; possible injection')
  27. os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'B' where username = '{name}'\"")
  28. def wyoj_adminize (name):
  29. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  30. raise Exception ('wyoj_adminize: Invalid username; possible injection')
  31. os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'S' where username = '{name}'\"")
  32. # 暂时不考虑嵌套。
  33. def wyoj_deadminize (name):
  34. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  35. raise Exception ('wyoj_deadminize: Invalid username; possible injection')
  36. os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'U' where username = '{name}'\"")
  37. def wyoj_admins ():
  38. return os.popen ("sudo docker exec uoj-db mysql -proot app_uoj233 -e \"select username from user_info where usergroup = 'S';\"").read ()
  39. def wyoj_banned ():
  40. return os.popen ("sudo docker exec uoj-db mysql -proot app_uoj233 -e \"select username from user_info where usergroup = 'B';\"").read ()
  41. async def db_insert_bind (uid, name):
  42. cursor = await sqlconn.cursor ()
  43. await cursor.execute (f'insert into binds (qq, name) values (\'{uid}\', \'{name}\')');
  44. await sqlconn.commit ()
  45. await cursor.close ()
  46. async def db_delete_bind (name):
  47. cursor = await sqlconn.cursor ()
  48. await cursor.execute (f'delete from binds where name = \'{name}\'');
  49. await sqlconn.commit ()
  50. await cursor.close ()
  51. async def db_query_by_id (uid):
  52. cursor = await sqlconn.cursor ()
  53. await cursor.execute (f'select qq, name from binds where qq = \'{uid}\'')
  54. res = await cursor.fetchall ()
  55. await cursor.close ()
  56. return res
  57. async def db_query_by_name (name):
  58. cursor = await sqlconn.cursor ()
  59. await cursor.execute (f'select qq, name from binds where name = \'{name}\'')
  60. res = await cursor.fetchall ()
  61. await cursor.close ()
  62. if len (res) > 1:
  63. raise Exception ('重复绑定')
  64. if len (res) == 1:
  65. return res[0]
  66. return None
  67. async def db_query_binds ():
  68. cursor = await sqlconn.cursor ()
  69. await cursor.execute (f'select qq, name from binds order by qq')
  70. res = await cursor.fetchall ()
  71. await cursor.close ()
  72. return res
  73. async def db_count_bound (qq):
  74. cursor = await sqlconn.cursor ()
  75. await cursor.execute (f'select count (*) from binds where qq = \'{qq}\'')
  76. res = await cursor.fetchone ()
  77. await cursor.close ()
  78. return res[0]
  79. async def db_check_bound (name):
  80. cursor = await sqlconn.cursor ()
  81. await cursor.execute (f'select count (*) from binds where name = \'{name}\'')
  82. res = await cursor.fetchone ()
  83. await cursor.close ()
  84. return res[0] > 0
  85. async def db_banned (qq):
  86. cursor = await sqlconn.cursor ()
  87. await cursor.execute (f'select count (*) from bans where qq = \'{qq}\'')
  88. res = await cursor.fetchone ()
  89. await cursor.close ()
  90. return res[0] > 0
  91. async def db_banned_all ():
  92. cursor = await sqlconn.cursor ()
  93. await cursor.execute (f'select qq from bans')
  94. res = await cursor.fetchall ()
  95. await cursor.close ()
  96. return res
  97. async def db_ban (qq):
  98. cursor = await sqlconn.cursor ()
  99. await cursor.execute (f'insert into bans (qq) values (\'{qq}\')');
  100. await sqlconn.commit ()
  101. await cursor.close ()
  102. async def db_unban (qq):
  103. cursor = await sqlconn.cursor ()
  104. await cursor.execute (f'delete from bans where qq = \'{qq}\'');
  105. await sqlconn.commit ()
  106. await cursor.close ()
  107. refpatt = re.compile (r'\[CQ:reply,id=([0-9]*)\]')
  108. atpatt = re.compile (r'\[CQ:at,qq=([0-9]*)\]')
  109. class MessageMeta:
  110. def __init__ (self, data):
  111. s = data['raw_message']
  112. self.data = data
  113. self.uid = data['user_id']
  114. self.msgtype = data['message_type']
  115. self.msgid = data['message_id']
  116. self.refs = re.findall (refpatt, s)
  117. self.ats = re.findall (atpatt, s)
  118. if self.msgtype != 'private':
  119. self.gid = data['group_id']
  120. i = 0
  121. flag = False
  122. while i < len (s) and (flag or s[i] == '['):
  123. flag |= s[i] == '['
  124. flag &= s[i] != ']'
  125. i += 1
  126. while i < len (s) and s[i] == ' ':
  127. i += 1
  128. self.msg = s[i:]
  129. self.cmd = self.msg.split ()
  130. async def reply_msg (meta, msg):
  131. return await ws.send (json.dumps ({
  132. 'action': 'send_group_msg',
  133. 'params': {
  134. 'group_id': meta.gid,
  135. 'message': [ { 'type': 'reply', 'data': { 'id': meta.msgid } },
  136. { 'type': 'text', 'data': { 'text': msg } } ]}}))
  137. async def reply_private_msg (meta, msg):
  138. return await ws.send (json.dumps ({
  139. 'action': 'send_private_msg',
  140. 'params': {
  141. 'user_id': meta.uid,
  142. 'message': [ { 'type': 'reply', 'data': { 'id': meta.msgid } },
  143. { 'type': 'text', 'data': { 'text': msg } } ]}}))
  144. def download_dir (s):
  145. return f'./download/{s}'
  146. FILE_KEEP_MIN = 60
  147. REPOTER_GAP = 5
  148. CHUNK_SIZE = 1048576 * 16
  149. async def receive_file (meta, data):
  150. fileid = data['message'][0]['data']['file_id']
  151. await ws.send (json.dumps ({
  152. 'action': 'get_file',
  153. 'params': { 'file_id': fileid }}))
  154. size = int (data['message'][0]['data']['file_size'])
  155. url = data['message'][0]['data']['url']
  156. name = data['message'][0]['data']['file']
  157. if not name.endswith ('.zip'):
  158. return await reply_msg (meta, f'识别到非 ZIP 格式文件,未下载')
  159. await reply_msg (meta, f'识别到 ZIP 格式文件,大小 %.3lf MB 正在下载' % (size / 1048576))
  160. start_time = time.time ()
  161. rx = 0
  162. finished = False
  163. async def reporter ():
  164. while not finished:
  165. duration = time.time () - start_time
  166. if rx != 0:
  167. await reply_msg (meta, f'已下载 %.3lf MB = %.3lf MB/s = %.3lf Mbps, ETA %.2lf 秒'
  168. % (rx / 1048576, rx / 1048576 / duration, rx * 8 / 1048576 / duration, duration / rx * size))
  169. await asyncio.sleep (REPOTER_GAP)
  170. asyncio.create_task (reporter ())
  171. async with aiohttp.ClientSession () as sess:
  172. async with sess.get (url) as resp:
  173. if resp.status != 200:
  174. await reply_msg (meta, f'请求 {url} 失败。HTTP 状态码 {resp.status}')
  175. filename = download_dir (str (meta.msgid))
  176. with open (filename, 'wb') as f:
  177. async for chunk in resp.content.iter_chunked (CHUNK_SIZE):
  178. f.write (chunk)
  179. rx += len (chunk)
  180. duration = time.time () - start_time
  181. finished = True
  182. await reply_msg (meta, f'已成功下存到 {filename},耗时 %.3lf 秒,合 %.3lf MB/s = %.3lf Mbps\n将在 {FILE_KEEP_MIN} 分钟后删除。'
  183. % (duration, size / duration / 1048576, size * 8 / 1048576 / duration))
  184. await asyncio.sleep (FILE_KEEP_MIN * 60)
  185. await reply_msg (meta, f'到时,删除文件 {filename}')
  186. os.system (f'rm {download_dir (filename)}')
  187. def random_token ():
  188. s = ''
  189. for i in range (16):
  190. s += random.choice ('abcdefghijklmnopqrstuvwxyz')
  191. return s
  192. def gen_confirm_token (meta):
  193. while True:
  194. s = str (meta.uid) + random_token ()
  195. s = base64.b64encode (s.encode ('utf-8')).decode ('utf-8')
  196. if s not in confirm_queue:
  197. return s
  198. CONFIRM_TIMEOUT = 60
  199. async def confirm (meta, msg):
  200. token = gen_confirm_token (meta)
  201. future = asyncio.Future ()
  202. confirm_queue[token] = future
  203. await reply_msg (meta, f'{msg}\n{CONFIRM_TIMEOUT} 秒内 /confirm {token} 来确认')
  204. try:
  205. result = await asyncio.wait_for (future, timeout=CONFIRM_TIMEOUT)
  206. except asyncio.TimeoutError:
  207. await reply_msg (meta, f'超时,取消操作')
  208. result = False
  209. else:
  210. await reply_msg (meta, f'成功确认')
  211. finally:
  212. del confirm_queue[token]
  213. return result
  214. def register_cmd (cmd):
  215. def decorator (fn):
  216. if cmd in commands:
  217. raise Exception(f'试图第二次注册命令 {cmd}')
  218. commands[cmd] = fn
  219. def wrapper (*args, **kwargs):
  220. return fn (*args, **kwargs)
  221. return wrapper
  222. return decorator
  223. def trusted_sender (fn):
  224. async def wrapper (*args, **kwargs):
  225. meta = args[0]
  226. if meta.uid in trusted_senders:
  227. return await fn (*args, **kwargs)
  228. return await reply_msg (args[0], f'执行该命令的权限不足')
  229. return wrapper
  230. @register_cmd ('/confirm')
  231. async def cmd_confirm (meta, arg):
  232. if len (arg) != 2:
  233. return await reply_msg (meta, f'需要令牌')
  234. token = arg[1]
  235. if token not in confirm_queue:
  236. return await reply_msg (meta, f'无效令牌')
  237. uid = base64.b64decode (token).decode ('utf-8')[:-16]
  238. if uid != str (meta.uid):
  239. return await reply_msg (meta, f'错误的确认者。期望 {uid},得到 {meta.uid}')
  240. confirm_queue[token].set_result (True)
  241. @register_cmd ('/ping')
  242. async def cmd_ping (meta, arg):
  243. s = f'PONG {meta.uid}'
  244. if meta.uid in trusted_senders:
  245. s += ' (privileged)'
  246. if await db_banned (meta.uid):
  247. s += ' (banned)'
  248. await reply_msg (meta, s)
  249. MAX_BINDS = 3
  250. @register_cmd ('/bind')
  251. async def cmd_bind (meta, arg):
  252. if len (arg) != 2:
  253. return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /bind username')
  254. name = arg[1]
  255. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  256. return await reply_msg (meta, '不合法用户名')
  257. if not await confirm (meta, f'确认将 {meta.uid} 绑定到 {name} 吗?'):
  258. return
  259. if await db_check_bound (name):
  260. qq, _ = await db_query_by_name (name)
  261. return await reply_msg (meta, f'无法绑定 {name};已绑定到 {qq}')
  262. if await db_count_bound (meta.uid) >= MAX_BINDS:
  263. return await reply_msg (meta, f'无法绑定更多账号。单个 QQ 绑定最大值为 {MAX_BINDS}')
  264. await db_insert_bind (meta.uid, name)
  265. wyoj_enable (meta.uid, name)
  266. await reply_msg (meta, f'成功将 {name} 绑定到 {meta.uid}')
  267. @register_cmd ('/rebind')
  268. @trusted_sender
  269. async def cmd_rebind (meta, arg):
  270. if len (arg) != 2:
  271. return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /bind username')
  272. name = arg[1]
  273. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  274. return await reply_msg (meta, '不合法用户名')
  275. if not await db_check_bound (name):
  276. return await reply_msg (meta, f'无绑定')
  277. qq, _ = await db_query_by_name (name)
  278. await db_delete_bind (name)
  279. await db_insert_bind (qq, name)
  280. wyoj_enable (qq, name)
  281. await reply_msg (meta, f'已重新绑定')
  282. @register_cmd ('/refresh')
  283. @trusted_sender
  284. async def cmd_refresh (meta, arg):
  285. q = await db_query_binds ()
  286. s = ''
  287. cnt = 0
  288. for qq, name in q:
  289. await db_delete_bind (name)
  290. await db_insert_bind (qq, name)
  291. wyoj_enable (qq, name)
  292. s += f'{qq}\t\t{name}\n'
  293. cnt += 1
  294. await reply_msg (meta, f'已成功绑定:\n{s}\n计 {cnt} 人')
  295. @register_cmd ('/unbind')
  296. async def cmd_unbind (meta, arg):
  297. if len (arg) != 2:
  298. return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /unbind username')
  299. name = arg[1]
  300. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  301. return await reply_msg (meta, '不合法用户名')
  302. if not await db_check_bound (name):
  303. return await reply_msg (meta, '该账号未绑定')
  304. qq, _ = await db_query_by_name (name)
  305. if qq != str (meta.uid):
  306. if meta.uid in trusted_senders:
  307. await reply_msg (meta, f'你是管理员 {meta.uid},强制解绑,等待确认')
  308. else:
  309. return await reply_msg (meta, f'{name} 与 {qq} 的绑定无法由你 ({meta.uid}) 解绑')
  310. if not await confirm (meta, f'确认将 {meta.uid} 从 {name} 解绑吗?'):
  311. return
  312. if not await db_check_bound (name):
  313. return await reply_msg (meta, f'尚无绑定,无法解绑')
  314. await db_delete_bind (name)
  315. wyoj_disable (name)
  316. await reply_msg (meta, f'成功将 {meta.uid} 与 {name} 解绑')
  317. @register_cmd ('/adminize')
  318. @trusted_sender
  319. async def cmd_adminize (meta, arg):
  320. if len (arg) not in [ 2, 3 ]:
  321. return await reply_msg (meta, f'需要用户名及时间\nUsage: /adminize username [time-in-minutes]')
  322. name = arg[1]
  323. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  324. return await reply_msg (meta, '不合法用户名')
  325. time = -1
  326. if len (arg) == 3:
  327. try:
  328. time = int (arg[2])
  329. except ValueError:
  330. return await reply_msg (meta, f'不合法的时间 {arg[2]}')
  331. if not await confirm (meta, f'确定要赋予 {name} 以 {time} 分钟的管理员权限吗?'):
  332. return
  333. wyoj_adminize (name)
  334. await reply_msg (meta, f'{name} 已被赋予管理员权限')
  335. if time > 0:
  336. await asyncio.sleep (time * 60)
  337. wyoj_deadminize (name)
  338. await reply_msg (meta, f'{name} 的管理员权限已到期')
  339. @register_cmd ('/deadminize')
  340. @trusted_sender
  341. async def cmd_deadminize (meta, arg):
  342. if len (arg) not in [ 2, 3 ]:
  343. return await reply_msg (meta, f'需要用户名\nUsage: /deadminize username')
  344. name = arg[1]
  345. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  346. return await reply_msg (meta, '不合法用户名')
  347. if not await confirm (meta, f'确定要取消 {name} 的管理员权限吗?'):
  348. return
  349. wyoj_deadminize (name)
  350. await reply_msg (meta, f'{name} 已被赋予管理员权限')
  351. async def cmd_query_qq (meta, arg):
  352. qq = arg[2]
  353. try:
  354. int (qq)
  355. except ValueError:
  356. await reply_msg (meta, f'无效 QQ 号 {qq}')
  357. res = await db_query_by_id (qq)
  358. await reply_msg (meta, f'QQ {qq} 绑定到 {len (res)} 个账号:{", ".join (map (lambda x: x[1], res))}')
  359. async def cmd_query_me (meta, arg):
  360. arg.append ('qq')
  361. arg.append (str (meta.uid))
  362. await cmd_query_qq (meta, arg)
  363. async def cmd_query_name (meta, arg):
  364. name = arg[2]
  365. if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
  366. return await reply_msg (meta, '不合法用户名')
  367. res = await db_query_by_name (name)
  368. if not res:
  369. await reply_msg (meta, f'{name} 未绑定')
  370. else:
  371. await reply_msg (meta, f'{name} 绑定到 {res[0]}')
  372. @trusted_sender
  373. async def cmd_query_all (meta, arg):
  374. await reply_msg (meta, f'绑定表:\n{"\n".join (map (lambda x: x[0] + "\t\t" + x[1], await db_query_binds ()))}')
  375. @trusted_sender
  376. async def cmd_query_banned (meta, arg):
  377. await reply_msg (meta, wyoj_banned ())
  378. @trusted_sender
  379. async def cmd_query_admins (meta, arg):
  380. await reply_msg (meta, wyoj_admins ())
  381. @register_cmd ('/query')
  382. async def cmd_query (meta, arg):
  383. if len (arg) == 1:
  384. return await cmd_query_me (meta, arg)
  385. elif len (arg) == 2:
  386. if arg[1] == 'all':
  387. return await cmd_query_all (meta, arg)
  388. elif arg[1] == 'banned':
  389. return await cmd_query_banned (meta, arg)
  390. elif arg[1] == 'admins':
  391. return await cmd_query_admins (meta, arg)
  392. elif len (arg) == 3:
  393. if arg[1] == 'qq':
  394. return await cmd_query_qq (meta, arg)
  395. elif arg[1] == 'name':
  396. return await cmd_query_name (meta, arg)
  397. await reply_msg (meta, '期望零个或两个参数\nUsage: /query || /query qq qqid || /query name username')
  398. @register_cmd ('/ban')
  399. @trusted_sender
  400. async def cmd_ban (meta, arg):
  401. if len (arg) != 2:
  402. return await reply_msg (meta, f'需要一个参数\nUsage: /ban username')
  403. qq = arg[1]
  404. try:
  405. int (qq)
  406. except ValueError:
  407. return await reply_msg (meta, f'无效 QQ 号 {qq}')
  408. if not await confirm (meta, f'确定要封禁 {qq} 及其名下的所有账号吗?'):
  409. return
  410. await db_ban (qq)
  411. res = await db_query_by_id (qq)
  412. for _, name in res:
  413. wyoj_disable (name)
  414. await reply_msg (meta, f'被封禁的用户:{", ".join (map (lambda x: x[1], res))}')
  415. @register_cmd ('/unban')
  416. @trusted_sender
  417. async def cmd_unban (meta, arg):
  418. if len (arg) != 2:
  419. return await reply_msg (meta, f'需要一个参数\nUsage: /unban username')
  420. try:
  421. qq = str (int (arg[1]))
  422. except ValueError:
  423. return await reply_msg (meta, f'无效 QQ 号 {qq}')
  424. if not await confirm (meta, f'确定要解封 {qq} 吗(不解封账号)?'):
  425. return
  426. await db_unban (qq)
  427. await reply_msg (meta, '成功解封')
  428. @register_cmd ('/banlist')
  429. async def cmd_banlist (meta, arg):
  430. await reply_msg (meta, f'被封禁的 QQ:{", ".join (map (lambda x: x[0], await db_banned_all ()))}')
  431. @register_cmd ('/entrust')
  432. @trusted_sender
  433. async def cmd_entrust (meta, arg):
  434. global trusted_senders
  435. if len (meta.ats) == 0:
  436. await reply_msg (meta, f'需要 At 某人\nUsage: [@someone..] /entrust [@someone..]')
  437. if not await confirm (meta, f'确定信任 {' '.join (meta.ats)} 吗?'):
  438. return
  439. for i in meta.ats:
  440. try:
  441. int (i)
  442. except ValueError:
  443. return await reply_msg (meta, f'非法 At')
  444. trusted_senders += map (int, meta.ats)
  445. await reply_msg (meta, f'成功信任')
  446. @register_cmd ('/detrust')
  447. @trusted_sender
  448. async def cmd_detrust (meta, arg):
  449. global trusted_senders
  450. if len (meta.ats) == 0:
  451. await reply_msg (meta, f'需要 At 某人\nUsage: [@someone..] /entrust [@someone..]')
  452. if not await confirm (meta, f'确定取消信任 {' '.join (meta.ats)} 吗?'):
  453. return
  454. for i in meta.ats:
  455. try:
  456. int (i)
  457. except ValueError:
  458. return await reply_msg (meta, f'非法 At')
  459. if int (i) not in trusted_senders:
  460. await reply_msg (meta, '其实 {i} 并未被信任。')
  461. for i in meta.ats:
  462. trusted_senders.remove (int (i))
  463. await reply_msg (meta, f'成功取消信任')
  464. @register_cmd ('/trusts')
  465. async def cmd_trusts (meta, arg):
  466. await reply_msg (meta, f'信任用户:{' '.join (map (str, trusted_senders))}')
  467. async def run_cmd (cmd):
  468. proc = await asyncio.create_subprocess_shell (
  469. cmd,
  470. stdout=asyncio.subprocess.PIPE,
  471. stderr=asyncio.subprocess.PIPE)
  472. stdout, stderr = await proc.communicate ()
  473. return stdout.decode ('utf-8'), stderr.decode ('utf-8')
  474. @register_cmd ('/upload')
  475. @trusted_sender
  476. async def cmd_upload (meta, arg):
  477. pid = arg[1]
  478. try:
  479. pid = int (pid)
  480. except ValueError:
  481. return await reply_msg (meta, f'非法题号')
  482. if len (meta.refs) != 1:
  483. return await reply_msg (meta, f'需要有且仅有一个指向题目数据压缩包的引用')
  484. if not os.path.exists (download_dir (meta.refs[0])):
  485. return await reply_msg (meta, f'数据文件未被下载:错误的引用或者已经超时')
  486. await reply_msg (meta, f'正在上传数据')
  487. stdout, stderr = await run_cmd (f'/home/ryp/upload {pid} {download_dir (meta.refs[0])}')
  488. await reply_msg (meta, f'命令执行完毕。标准输出:\n{stdout}\n\n标准错误:{stderr}\n' \
  489. f'若无错误,请打开 https://oj.ryp.org.cn/problem/{pid}/manage/data 点击校验配置并同步数据')
  490. async def dispatch (meta):
  491. if not meta.cmd[0] in commands:
  492. return await reply_msg (meta, f'未知命令 {meta.cmd[0]}')
  493. await commands[meta.cmd[0]] (meta, meta.cmd)
  494. PRIVATE_TIMEOUT = 60 * 15
  495. async def await_private_msg (qq):
  496. if qq in private_queue.keys ():
  497. raise Exception ('重复期望一条私聊消息')
  498. future = asyncio.Future ()
  499. private_queue[qq] = future
  500. try:
  501. res = await asyncio.wait_for (future, timeout=PRIVATE_TIMEOUT)
  502. except asyncio.TimeoutError:
  503. await reply_msg (meta, f'十五分钟未回复。本次操作取消')
  504. res = False
  505. finally:
  506. del private_queue[qq]
  507. return res
  508. async def begin_private (meta):
  509. qq = meta.uid
  510. await reply_private_msg (meta, '请把题目数据发给我(内含 problem.conf 的 ZIP 存档)')
  511. data_msg = await await_private_msg (qq)
  512. if data_msg.data['message'][0]['type'] != 'file':
  513. return await reply_private_msg (data_msg, '你发的不是文件。本次对话结束')
  514. await receive_file (data_msg, data_msg.data)
  515. await reply_private_msg (meta, '收到了!')
  516. async def dispatch_private (meta):
  517. qq = meta.uid
  518. if len (await db_query_by_id (qq)) == 0:
  519. return await reply_private_msg (meta, '你必须绑定后才能私聊我')
  520. try:
  521. private_queue[qq].set_result (meta)
  522. except KeyError:
  523. await begin_private (meta)
  524. async def handle_msg (data):
  525. try:
  526. print (data)
  527. data = json.loads (data)
  528. meta = MessageMeta (data)
  529. if meta.msgtype == 'private':
  530. print ('是私聊消息')
  531. await dispatch_private (meta)
  532. return
  533. if meta.gid != GROUP_ID:
  534. return
  535. if data['message'][0]['type'] == 'file':
  536. return await receive_file (meta, data)
  537. if meta.msg[0] != '/':
  538. return
  539. print (f'收到群 {meta.gid} 的用户 {meta.uid} 的命令:{' '.join (meta.cmd)}')
  540. await dispatch (meta)
  541. except KeyError as e:
  542. print (f'键错误:{e}')
  543. traceback.print_exc ()
  544. except Exception as e:
  545. print (f'未知错误:{e}')
  546. traceback.print_exc ()
  547. async def client_loop ():
  548. global ws, sqlconn
  549. ws = await websockets.connect (wsserver)
  550. sqlconn = await aiosqlite.connect ('data.db')
  551. print ('已成功连接到 NapCat 服务器')
  552. async for msg in ws:
  553. asyncio.create_task (handle_msg (msg))
  554. if __name__ == '__main__':
  555. asyncio.run (client_loop ())