diff --git a/aiotieba-handle-exception-expose.patch b/aiotieba-handle-exception-expose.patch new file mode 100644 index 0000000..fccda4b --- /dev/null +++ b/aiotieba-handle-exception-expose.patch @@ -0,0 +1,40 @@ +--- venv/lib/python3.11/site-packages/aiotieba/helper/utils.py ++++ venv/lib/python3.11/site-packages/aiotieba/helper/utils.py +@@ -141,35 +141,6 @@ + + def wrapper(func): + async def awrapper(*args, **kwargs): +- try: +- ret = await func(*args, **kwargs) +- +- except Exception as err: +- meth_name = func.__name__ +- tb = err.__traceback__ +- while tb := tb.tb_next: +- frame = tb.tb_frame +- if frame.f_code.co_name == meth_name: +- break +- frame = tb.tb_next.tb_frame +- +- log_str: str = frame.f_locals.get('__log__', '') +- if not no_format: # need format +- log_str = log_str.format(**frame.f_locals) +- log_str = f"{err}. {log_str}" +- +- logger = get_logger() +- if logger.isEnabledFor(log_level): +- record = logger.makeRecord(logger.name, log_level, None, 0, log_str, None, None, meth_name) +- logger.handle(record) +- +- exc_handlers._handle(meth_name, err) +- +- return null_ret_factory() +- +- else: +- return ret +- ++ return await func(*args, **kwargs) + return awrapper +- + return wrapper + diff --git a/app.py b/app.py index 28f89b5..768efb7 100644 --- a/app.py +++ b/app.py @@ -119,14 +119,12 @@ async def thread_view(tid): all_users = {} for floor in thread_info: for comment in floor.comments: - if not comment.reply_to_id in available_users: + if comment.reply_to_id and not comment.reply_to_id in available_users: all_users[comment.reply_to_id] = '' - all_users.pop(0, None) all_users = list(all_users.keys()) await asyncio.gather(*(cache_name_from_id(tieba, i) for i in all_users)) - - + return await render_template('thread.html', info=thread_info) @app.route('/f') @@ -138,8 +136,28 @@ async def forum_view(): async with aiotieba.Client() as tieba: forum_info, threads = await asyncio.gather(awaitify(find_tieba_info)(fname), tieba.get_threads(fname, rn=50, pn=pn, sort=sort)) + if threads.page.current_page > threads.page.total_page or pn < 1: + return await render_template('error.html', msg = \ + f'请求越界,本贴吧共有 { threads.page.total_page } 页' + f'而您查询了第 { threads.page.current_page} 页') return await render_template('bar.html', info=forum_info, threads=threads, sort=sort) +@app.route('/home/main') +async def user_view(): + return 'UNDER CONSTRUCTION' + +###################################################################### + +@app.errorhandler(RuntimeError) +async def runtime_error_view(e): + if e.msg: + return await render_template('error.html', msg=e.msg) + return await render_template('error.html', msg='错误信息不可用') + +@app.errorhandler(Exception) +async def general_error_view(e): + return await render_template('error.html', msg=e) + if __name__ == '__main__': app.run(debug=True) diff --git a/extra.py b/extra.py index 9cd533b..2565ada 100644 --- a/extra.py +++ b/extra.py @@ -14,7 +14,7 @@ def extract_image_name(url): except: return '404.jpg' -@cache.cached(timeout=60, key_prefix='tieba_info') +@cache.memoize(timeout=60) def find_tieba_info(tname): """Get the tiebat avatar for the forum name. @@ -24,7 +24,14 @@ def find_tieba_info(tname): """ info = { 'name': tname } - res = requests.get('https://tieba.baidu.com/f', params={'kw': tname}) + res = requests.get('https://tieba.baidu.com/f', + params={'kw': tname}, + allow_redirects=False) + + # Baidu will bring us to the search page, so we ignore it. + if res.status_code == 302: + raise ValueError('您搜索的贴吧不存在') + soup = bs4.BeautifulSoup(res.text, 'html.parser') elems = soup.select('#forum-card-head') diff --git a/static/css/main.css b/static/css/main.css index 6b188a2..793188b 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -17,6 +17,18 @@ color: inherit !important; } +.post :nth-child(1) { + flex: 0 0 auto; +} + +.post .tier { + text-align: center; + padding: .3rem; + font-size: .8rem; + background-color: var(--replies-color); +} + + /* global styling */ :root { --bg-color: #eeeecc; diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..ce6b440 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,25 @@ + + + 错误 - RAT + + + + + + + + +
+
+ + 🙃 + +
+

{{ msg }}

+
+ + diff --git a/templates/thread.html b/templates/thread.html index fc06ed5..8abcdfd 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -17,7 +17,13 @@
{% for p in info %}
- +
+ + {% if p.user.is_bawu %} +
吧务
+ {% endif %} +
level {{ p.user.level }}
+
{{ p.user.user_name }} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..c5964cd --- /dev/null +++ b/utils.py @@ -0,0 +1,146 @@ +import asyncio +import functools +import logging +import sys +from types import FrameType +from typing import Any, Callable + +import async_timeout + +from ..exception import exc_handlers +from ..logging import get_logger + +try: + import simdjson as jsonlib + + _JSON_PARSER = jsonlib.Parser() + parse_json = _JSON_PARSER.parse + +except ImportError: + import json as jsonlib + + parse_json = jsonlib.loads + +pack_json = functools.partial(jsonlib.dumps, separators=(',', ':')) + +if sys.version_info >= (3, 9): + + def removeprefix(s: str, prefix: str) -> str: + """ + 移除字符串前缀 + + Args: + s (str): 待移除前缀的字符串 + prefix (str): 待移除的前缀 + + Returns: + str: 移除前缀后的字符串 + """ + + return s.removeprefix(prefix) + + def removesuffix(s: str, suffix: str) -> str: + """ + 移除字符串前缀 + + Args: + s (str): 待移除前缀的字符串 + suffix (str): 待移除的前缀 + + Returns: + str: 移除前缀后的字符串 + """ + + return s.removesuffix(suffix) + +else: + + def removeprefix(s: str, prefix: str) -> str: + """ + 移除字符串前缀 + + Args: + s (str): 待移除前缀的字符串 + prefix (str): 待移除的前缀 + + Returns: + str: 移除前缀后的字符串 + + Note: + 该函数不会拷贝字符串 + """ + + if s.startswith(prefix): + return s[len(prefix) :] + else: + return s + + def removesuffix(s: str, suffix: str) -> str: + """ + 移除字符串后缀 + 该函数将不会拷贝字符串 + + Args: + s (str): 待移除前缀的字符串 + suffix (str): 待移除的前缀 + + Returns: + str: 移除前缀后的字符串 + + Note: + 该函数不会拷贝字符串 + """ + + if s.endswith(suffix): + return s[: len(suffix)] + else: + return s + + +def is_portrait(portrait: str) -> bool: + """ + 简单判断输入是否符合portrait格式 + """ + + return isinstance(portrait, str) and portrait.startswith('tb.') + + +def timeout(delay: float, loop: asyncio.AbstractEventLoop) -> async_timeout.Timeout: + now = loop.time() + when = round(now) + delay + return async_timeout.timeout_at(when) + + +def log_success(frame: FrameType, log_str: str = '', log_level: int = logging.INFO): + """ + 成功日志 + + Args: + frame (FrameType): 帧对象 + log_str (str): 附加日志 + log_level (int): 日志等级 + """ + + meth_name = frame.f_code.co_name + log_str = "Suceeded. " + log_str + logger = get_logger() + if logger.isEnabledFor(log_level): + record = logger.makeRecord(logger.name, log_level, None, 0, log_str, None, None, meth_name) + logger.handle(record) + + +def handle_exception(null_ret_factory: Callable[[], Any], no_format: bool = False, log_level: int = logging.WARNING): + """ + 处理request抛出的异常 + + Args: + null_ret_factory (Callable[[], Any]): 空构造工厂 用于返回一个默认值 + no_format (bool, optional): 不需要再次格式化日志字符串 常见于不论成功与否都会记录日志的api. Defaults to False. + log_level (int, optional): 日志等级. Defaults to logging.WARNING. + """ + + def wrapper(func): + async def awrapper(*args, **kwargs): + return await func(*args, **kwargs) + return awrapper + return wrapper