def web(): M = Monitor() try: html = """ <style> table { border-collapse: collapse; } th, td { vertical-align: top; border: 1px solid black; } .mode-watch { background: #ffa; } .mode-blacklist { background: #fcc; } </style> <table> <tr> <th>wiki</th> <th>id</th> <th>name</th> <th>mode</th> </tr> """ M.cur.execute( """SELECT `af_id`, `af_name`, `mode`, `wiki` FROM `abusefilter` ORDER BY `wiki` ASC, `af_id` ASC""" ) rows = M.cur.fetchall() for row in rows: html += """ <tr class="mode-{2}"> <td>{3}</td> <td>{0}</td> <td>{1}</td> <td>{2}</td> </tr> """.format(row[0], row[1], row[2], row[3]) html += '</table>' return html except Exception: M.error(traceback.format_exc()) return "OK1"
def web(): M = Monitor() try: html = """ <style> table { border-collapse: collapse; } th, td { vertical-align: top; border: 1px solid black; } </style> status <a href="./log?type=log">log</a> <a href="./blacklist">blacklist</a> <table> <tr> <th>database</th> <th>last time</th> </tr> """ for table in tables: M.cur.execute("""SELECT MAX(`timestamp`) FROM """ + table) rows = M.cur.fetchall() if rows[0][0] is None: html += """ <tr> <td>{}</td> <td>No record</td> </tr> """.format(table) else: html += """ <tr> <td><a href="{0}log?type={1}" target="_blank">{1}</td> <td>{2}</td> </tr> """.format(M.siteurl, table, M.formattimediff(rows[0][0])) html += '</table>' return html except Exception: M.error(traceback.format_exc()) return "OK1"
sock.settimeout(10) M = Monitor() pool = [] while True: try: starttime = time.time() while True: try: rawdata, address = sock.recvfrom(config['max_bytes']) rawdata = rawdata.decode() try: data = json.loads(rawdata) except json.decoder.JSONDecodeError: M.error('[ores_handler] JSONDecodeError: {}'.format(rawdata)) continue pool.append(data) if len(pool) >= 10 or time.time() - starttime > 10: break except socket.timeout: break if len(pool): revids = '|'.join([str(data['revid']) for data in pool]) url = 'https://ores.wikimedia.org/v3/scores/zhwiki/?models=damaging&revids={}'.format(revids) req = urllib.request.Request(url, headers={'User-Agent': M.wp_user_agent}) rawresult = urllib.request.urlopen(req).read().decode("utf8") try: result = json.loads(rawresult) except json.decoder.JSONDecodeError:
def web(): M = Monitor() try: data = json.loads(request.data.decode("utf8")) if "message" in data: m_date = data["message"]["date"] m_chat_id = int(data["message"]["chat"]["id"]) m_user_id = int(data["message"]["from"]["id"]) if "reply_to_message" in data["message"]: reply_from = data["message"]["reply_to_message"]["from"] from_user_id = reply_from["id"] from_firstname = reply_from["first_name"] from_lastname = "" if "last_name" in reply_from: from_lastname = reply_from["last_name"] if "text" in data["message"]: M.chat_id = m_chat_id m_text = data["message"]["text"] M.log(m_text, logtype='webhook') if not m_text.startswith("/"): return "OK" def checkadmin(): M.cur.execute( """SELECT `name` FROM `admin` WHERE `user_id` = %s""", (str(m_user_id))) rows = M.cur.fetchall() if len(rows) == 0: M.sendmessage("你沒有權限") return None return rows[0][0] def handle_parser(parser, cmd): with io.StringIO() as buf, contextlib.redirect_stdout( buf), contextlib.redirect_stderr(buf): try: args = parser.parse_args(cmd) except SystemExit: output = buf.getvalue() M.sendmessage(output) return None except Exception: M.error(traceback.format_exc()) return None else: return args cmd = shlex.split(m_text) action = cmd[0] cmd = cmd[1:] action = action[1:] action = re.sub(r'@cvn_smart_bot$', '', action) action = action.lower() if action in ['gettoken'] and m_chat_id == m_user_id: if not checkadmin(): return "OK" M.cur.execute( """SELECT `token` FROM `admin` WHERE `user_id` = %s""", (str(m_user_id))) rows = M.cur.fetchall() if len(rows) > 0: M.sendmessage("您的存取權杖是\ncvn_smart(" + rows[0][0] + ")\n使用 /newtoken 取得新的", nolog=True) else: M.sendmessage("查詢存取權杖失敗,使用 /newtoken 取得新的", nolog=True) return "OK" if action in ['newtoken'] and m_chat_id == m_user_id: if not checkadmin(): return "OK" import random import string token = ''.join( random.choice(string.ascii_lowercase + string.digits) for _ in range(32)) M.cur.execute( """UPDATE `admin` SET `token` = %s WHERE `user_id` = %s""", (token, m_user_id)) M.db.commit() M.sendmessage("您的存取權杖是\ncvn_smart(" + token + ")\n舊的存取權杖已失效", nolog=True) return "OK" if m_chat_id not in M.response_chat_id: return "OK" if action in ['setadmin']: if not checkadmin(): return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('nickname', type=str, default=None, nargs='?', help='用戶暱稱') args = handle_parser(parser, cmd) if args is None: return 'OK' if "reply_to_message" not in data["message"]: M.sendmessage("需要reply訊息") return "OK" name = from_firstname if args.nickname is not None and args.nickname.strip( ) != '': name = args.nickname M.cur.execute( """REPLACE INTO `admin` (`user_id`, `name`) VALUES (%s, %s)""", (str(from_user_id), name)) M.db.commit() M.sendmessage("設定" + name + "(" + (from_firstname + " " + from_lastname).strip() + ")(" + str(from_user_id) + ")為管理員") return "OK" if action in ['deladmin']: if not checkadmin(): return "OK" if "reply_to_message" not in data["message"]: M.sendmessage("需要reply訊息") return "OK" from_user_id = ( data["message"]["reply_to_message"]["from"]["id"]) if from_user_id == m_user_id: M.sendmessage("你不能將自己除權") return "OK" count = M.cur.execute( """DELETE FROM `admin` WHERE `user_id` = %s""", (str(from_user_id))) M.db.commit() if count == 0: M.sendmessage("該用戶不是管理員") else: M.sendmessage("移除" + (from_firstname + " " + from_lastname).strip() + "(" + str(from_user_id) + ")為管理員") return "OK" if action in ['adduser', 'au']: name = checkadmin() if name is None: return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('username', type=str, default=None, nargs='?', help='用戶名') parser.add_argument('reason', type=str, default='無原因', nargs='?', help='原因') parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') parser.add_argument('-r', '--reason', type=str, metavar='原因', default='無原因', help='預設:%(default)s') parser.add_argument('-p', '--point', type=int, metavar='點數', default=10, help='預設:%(default)s') args = handle_parser(parser, cmd) if args is None: return 'OK' user = args.username if user is None and 'reply_to_message' in data['message']: user = M.get_user_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(user) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = user[0][0] if user is None: M.sendmessage('未指定用戶名') return "OK" user, wiki = M.parse_user(user) if args.wiki is not None: wiki = args.wiki reason = name + '加入:' + args.reason M.addblack_user(user, m_date, reason, wiki) M.adduser_score(M.user_type(user), args.point) return 'OK' if action in ['userscore', 'us']: name = checkadmin() if name is None: return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) if 'reply_to_message' in data['message']: user = M.get_user_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(user) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = user[0][0] parser.add_argument('point', type=int, nargs='?', default=10, help='點數,預設:%(default)s') else: parser.add_argument('username', type=str, help='用戶名') parser.add_argument('point', type=int, nargs='?', default=10, help='點數,預設:%(default)s') args = handle_parser(parser, cmd) if args is None: return 'OK' if 'reply_to_message' not in data['message']: user = args.username user, wiki = M.parse_user(user) point = args.point userobj = M.user_type(user) M.adduser_score(userobj, point) point2 = M.getuser_score(userobj) message = "為 {0} 調整分數 {1:+d} 為 {2}".format( user, point, point2) M.sendmessage(message) return "OK" if action in ['deluser', 'du']: if not checkadmin(): return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('username', type=str, default=None, nargs='?', help='用戶名') parser.add_argument('reason', type=str, default=None, nargs='?', help='原因') # Unused parser.add_argument('-r', '--reason', type=str, metavar='原因') # Unused parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') args = handle_parser(parser, cmd) if args is None: return 'OK' user = args.username if user is None and 'reply_to_message' in data['message']: user = M.get_user_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(user) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = user[0][0] if user is None: M.sendmessage('未指定用戶名') return "OK" user, wiki = M.parse_user(user) if args.wiki is not None: wiki = args.wiki M.delblack_user(user, wiki) return "OK" if action in ['setwiki']: if not checkadmin(): return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument( 'wiki', type=str, default='zhwiki', metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') parser.add_argument('-u', '--user', dest='username', type=str, metavar='用戶名') args = handle_parser(parser, cmd) if args is None: return 'OK' user = args.username if user is None and 'reply_to_message' in data['message']: user = M.get_user_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(user) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = user[0][0] if user is None: return 'OK' user, _ = M.parse_user(user) M.setwikiblack_user(user, args.wiki) return "OK" if action in ['addpage', 'ap']: name = checkadmin() if name is None: return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('pagetitle', type=str, default=None, nargs='?', help='頁面名') parser.add_argument('reason', type=str, default='無原因', nargs='?', help='原因') parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') parser.add_argument('-r', '--reason', type=str, metavar='原因', default='無原因', help='預設:%(default)s') parser.add_argument('-p', '--point', type=int, metavar='點數', default=30, help='預設:%(default)s') args = handle_parser(parser, cmd) if args is None: return 'OK' page = args.pagetitle if page is None and 'reply_to_message' in data['message']: page = M.get_page_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(page) == 0: M.sendmessage("無法從訊息找到所對應的頁面") return "OK" page = page[0][0] if page is None: M.sendmessage('未指定頁面標題') return 'OK' page, wiki = M.parse_page(page) if args.wiki is not None: wiki = args.wiki reason = name + '加入:' + M.parse_reason(args.reason) M.addblack_page(page, m_date, reason, wiki=wiki, point=args.point) return "OK" if action in ['massaddpage']: name = checkadmin() if name is None: return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('pagetitle', type=str, nargs='+', help='頁面名') parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') parser.add_argument('-r', '--reason', type=str, metavar='原因', default='無原因', help='預設:%(default)s') parser.add_argument('-p', '--point', type=int, metavar='點數', default=30, help='預設:%(default)s') args = handle_parser(parser, cmd) if args is None: return 'OK' for page in args.pagetitle: page, wiki = M.parse_page(page) if args.wiki is not None: wiki = args.wiki reason = name + '加入:' + M.parse_reason(args.reason) M.addblack_page(page, m_date, reason, wiki=wiki, point=args.point) return "OK" if action in ['delpage', 'dp']: if not checkadmin(): return "OK" parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('pagetitle', type=str, default=None, nargs='?', help='頁面名') parser.add_argument('reason', type=str, default=None, nargs='?', help='原因') # Unused parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') args = handle_parser(parser, cmd) if args is None: return 'OK' page = args.pagetitle if page is None and 'reply_to_message' in data['message']: page = M.get_page_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(page) == 0: M.sendmessage("無法從訊息找到所對應的頁面") return "OK" page = page[0][0] if page is None: M.sendmessage('未指定頁面標題') return 'OK' page, wiki = M.parse_page(page) if args.wiki is not None: wiki = args.wiki M.delblack_page(page, wiki) return "OK" if action in ['checkuser', 'cu']: parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('username', type=str, default=None, nargs='?', help='用戶名') parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') parser.add_argument('-r', '--reason', type=str, metavar='原因', help='篩選指定原因') args = handle_parser(parser, cmd) if args is None: return 'OK' user = args.username if user is None and 'reply_to_message' in data['message']: user = M.get_user_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(user) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = user[0][0] if user is None: M.sendmessage('未指定用戶名') return "OK" user, wiki = M.parse_user(user) if args.wiki is not None: wiki = args.wiki message = M.checkuser(user, wiki, reason=args.reason) M.sendmessage(message, user + '|' + wiki) return "OK" if action in ['checkpage', 'cp']: parser = argparse.ArgumentParser( prog='/{0}'.format(action)) parser.add_argument('pagetitle', type=str, default=None, nargs='?', help='頁面名') parser.add_argument( '-w', '--wiki', type=str, metavar='站點', help= '參見 https://quarry.wmflabs.org/query/278 ,預設:zhwiki') args = handle_parser(parser, cmd) if args is None: return 'OK' page = args.pagetitle if page is None and 'reply_to_message' in data['message']: page = M.get_page_from_message_id( data["message"]["reply_to_message"]["message_id"]) if len(page) == 0: M.sendmessage("無法從訊息找到所對應的頁面") return "OK" page = page[0][0] if page is None: M.sendmessage('未指定頁面標題') return 'OK' page, wiki = M.parse_page(page) if args.wiki is not None: wiki = args.wiki message = "" rows = M.check_page_blacklist(page, wiki) if len(rows) != 0: message += "\n於黑名單:" for record in rows: message += ("\n" + M.parse_wikicode(record[0]) + ', ' + M.formattimediff(record[1])) if message != "": M.sendmessage(page + "@" + wiki + message) else: M.sendmessage(page + "@" + wiki + ":查無結果") return "OK" if action in ['os'] and 'reply_to_message' in data['message']: if not checkadmin(): return "OK" M.deletemessage(data["message"]["message_id"]) M.deletemessage( data["message"]["reply_to_message"]["message_id"]) return "OK" if action in ['osall' ] and 'reply_to_message' in data['message']: if not checkadmin(): return "OK" M.deletemessage(data["message"]["message_id"]) message_id = ( data["message"]["reply_to_message"]["message_id"]) M.cur.execute( """SELECT `user` FROM `bot_message` WHERE `message_id` = %s""", (message_id)) rows = M.cur.fetchall() if len(rows) == 0: M.sendmessage("無法從訊息找到所對應的對象") return "OK" user = rows[0][0] M.cur.execute( """SELECT `message_id` FROM `bot_message` WHERE `user` = %s""", (user)) rows = M.cur.fetchall() for row in rows: M.deletemessage(row[0]) return "OK" if action in ['status']: message = (('Webhook: <a href="{}">WORKING!!</a>\n' + '<a href="{}status">查看資料接收狀況</a>').format( 'https://zh.wikipedia.org/wiki/WORKING!!', M.siteurl)) M.sendmessage(message) return "OK" return "OK" except Exception: traceback.print_exc() M.error(traceback.format_exc()) return "OK"
def web(): M = Monitor() try: data = request.form.to_dict() if "token" not in data: return json.dumps({"message": "沒有給予存取權杖"}) m = re.search(r"cvn_smart\(([a-z0-9]{32})\)", data["token"]) if m is not None: data["token"] = m.group(1) def checkadmin(): M.cur.execute("""SELECT `name` FROM `admin` WHERE `token` = %s""", (data["token"])) rows = M.cur.fetchall() if len(rows) == 0: return None return rows[0][0] if "action" not in data: return json.dumps({"message": "沒有給予操作類型"}) if data["action"] == "authorize": name = checkadmin() if name is None: return json.dumps({"result": "fail"}) return json.dumps({"result": "success", "user": name}) if data["action"] == "centralauthorize": query = { "format": "json", "action": "query", "meta": "userinfo", "centralauthtoken": data["centralauthtoken"] } query = urllib.parse.urlencode(query) url = "https://meta.wikimedia.org/w/api.php?" + query res = urllib.request.urlopen(url).read().decode("utf8") res = json.loads(res) if "query" in res: wiki_username = res["query"]["userinfo"]["name"] M.cur.execute( """SELECT `name`, `token` FROM `admin` WHERE `wiki_username` = %s""", (wiki_username)) rows = M.cur.fetchall() if len(rows) == 0: return json.dumps({"result": "fail"}) return json.dumps({ "result": "success", "user": rows[0][0], "token": rows[0][1] }) return json.dumps({"result": "fail"}) if data["action"] == "updatecentralinfo": name = checkadmin() if name is None: return json.dumps({"result": "fail"}) query = { "format": "json", "action": "query", "meta": "userinfo", "centralauthtoken": data["centralauthtoken"] } query = urllib.parse.urlencode(query) url = "https://meta.wikimedia.org/w/api.php?" + query res = urllib.request.urlopen(url).read().decode("utf8") res = json.loads(res) if "query" in res: wiki_username = res["query"]["userinfo"]["name"] count = M.cur.execute( """UPDATE `admin` SET `wiki_username` = %s WHERE `token` = %s""", (wiki_username, data["token"])) M.db.commit() if count == 0: return json.dumps({"result": "fail"}) return json.dumps({ "result": "success", "wiki_username": wiki_username, "user": name }) return json.dumps({"result": "fail"}) if data["action"] == "addpage": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) page, wiki = M.parse_page(data["page"]) try: point = int(data["point"]) except ValueError: point = 30 reason = name + "加入:" + M.parse_reason(data["reason"]) message = M.addblack_page(page, int(time.time()), reason, point, wiki, msgprefix=name + "透過API") return json.dumps({"message": message}) if data["action"] == "delpage": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) page, wiki = M.parse_page(data["page"]) message = M.delblack_page(page, wiki, msgprefix=name + "透過API將") return json.dumps({"message": message}) if data["action"] == "pagescore": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) page, wiki = M.parse_user(data["page"]) try: point = int(data["point"]) except ValueError: point = 30 M.addpage_score(page, wiki, point) point2 = M.getpage_score(page, wiki) message = "為 {0} 調整分數 {1:+d} 為 {2}".format(page, point, point2) return json.dumps({"message": message}) if data["action"] == "adduser": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) user, wiki = M.parse_user(data["user"]) reason = name + "加入:" + M.parse_reason(data["reason"]) message = M.addblack_user(user, int(time.time()), reason, wiki, msgprefix=name + "透過API") try: point = int(data["point"]) except ValueError: point = 10 M.adduser_score(M.user_type(user), point) return json.dumps({"message": message}) if data["action"] == "deluser": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) user, wiki = M.parse_user(data["user"]) message = M.delblack_user(user, wiki, msgprefix=name + "透過API將") return json.dumps({"message": message}) if data["action"] == "userscore": name = checkadmin() if name is None: return json.dumps({"message": "你沒有權限", "nopermission": True}) user, wiki = M.parse_user(data["user"]) try: point = int(data["point"]) except ValueError: point = 10 userobj = M.user_type(user) M.adduser_score(userobj, point) point2 = M.getuser_score(userobj) message = "為 {0} 調整分數 {1:+d} 為 {2}".format(user, point, point2) return json.dumps({"message": message}) data["token"] = "" M.log(json.dumps(data, ensure_ascii=False)) return json.dumps({"message": "伺服器沒有進行任何動作"}) except Exception: traceback.print_exc() M.error(traceback.format_exc()) return json.dumps({"message": traceback.format_exc()})
'format': 'json' } try: res = session.get(M.wp_api, params=params).json() except json.decoder.JSONDecodeError as e: logging.error('JSONDecodeError: {}'.format(e)) time.sleep(args.sleep) continue except requests.exceptions.ConnectionError as e: logging.error('ConnectionError: {}'.format(e)) time.sleep(args.sleep) continue if 'error' in res: logging.error('{}'.format(res['error'])) M.error(res['error']) if 'query' not in res: logging.info('{}'.format(res)) time.sleep(args.sleep) continue for log in res["query"]["abuselog"]: newlog = log.copy() if newlog['filter_id'] == '': newlog['filter_id'] = M.get_af_id_by_name( newlog['filter'], args.wiki) if newlog['filter_id'].startswith('global-'): newlog['filter_id'] = int(newlog['filter_id'][7:])
def web(): M = Monitor() try: result = """ <a href="./status">status</a> log <a href="./blacklist">blacklist</a> <form> """ for table in tables: result += ('<button type="submit" name="type" value="{0}">' + '{0}</button> ').format(table) result += '</form>' if "type" in request.args: logtype = request.args["type"] if logtype in tables: M.cur2 = M.db.cursor(pymysql.cursors.DictCursor) M.cur2.execute( """SELECT * FROM {} ORDER BY `timestamp` DESC LIMIT 20""" .format(logtype) ) rows = M.cur2.fetchall() if len(rows) == 0: result += 'No record' else: result += """ <style> table { border-collapse: collapse; } th, td { vertical-align: top; border: 1px solid black; } </style> """ result += '<table>' result += '<tr>' for col in rows[0]: if col == "parsedcomment": continue result += '<th>' + col + '</th>' result += '</tr>' for row in rows: result += '<tr>' for col in row: if col == "parsedcomment": continue elif (logtype == "error" and col == "error"): result += ('<td><pre>' + html.escape(row[col], quote=False) + '</pre></td>') elif col in ['log_action_comment', 'comment']: result += ('<td>' + html.escape(row[col], quote=False) + '</td>') elif col == "timestamp": result += '<td>{} ({})</td>'.format( str(row[col]), M.formattimediff(row[col])) else: result += '<td>' + str(row[col]) + '</td>' result += '</tr>' result += '</table>' return result except Exception: M.error(traceback.format_exc()) return traceback.format_exc()
M = Monitor() logging.getLogger().addHandler(MonitorLogHandler(M, 'allwiki')) os.environ['TZ'] = 'UTC' url = 'https://stream.wikimedia.org/v2/stream/recentchange' sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect((SOCKET_HOST, SOCKET_PORT)) logging.info('My address is {}'.format(sock.getsockname())) errorWaitTime = 1 while True: try: for event in EventSource(url): if event.event == 'message': if len(event.data) == 0: continue logging.debug(event.data) noError = True data = event.data.encode('utf-8') process(data) except Exception as e: traceback.print_exc() M.error(traceback.format_exc(), noRaise=True)