From 965c8f2cc0384553fe0423e436807855f35c2140 Mon Sep 17 00:00:00 2001
From: John Xina <bingchilling@riseup.net>
Date: Thu, 13 Jul 2023 18:48:49 +0800
Subject: [PATCH] Multiple features

---
 aiotieba-handle-exception-expose.patch |  40 +++++++
 app.py                                 |  26 ++++-
 extra.py                               |  11 +-
 static/css/main.css                    |  12 ++
 templates/error.html                   |  25 +++++
 templates/thread.html                  |   8 +-
 utils.py                               | 146 +++++++++++++++++++++++++
 7 files changed, 261 insertions(+), 7 deletions(-)
 create mode 100644 aiotieba-handle-exception-expose.patch
 create mode 100644 templates/error.html
 create mode 100644 utils.py

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 @@
+<!DOCTYPE html>
+<head>
+  <title>错误 - RAT</title>
+
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <link href="/static/css/main.css" rel="stylesheet">
+</head>
+
+<body>
+  <div class="list" style="margin: 5%; padding: 1%; text-align: center;">
+    <div style="font-size: 10em;">
+      <a title="带我回去" href="javascript:window.history.back();" style="text-decoration: none;">
+	&#x1F643;
+      </a>
+    </div>
+    <h1>{{ msg }}</h1>
+  </div>
+  <footer>
+    <div><a href="#">RAT Ain't Tieba</a></div>
+    <div><a href="#">自豪地以 AGPLv3 释出</a></div>
+    <div><a href="#">源代码</a></div>
+  </footer>
+</body>
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 @@
   <div class="list">
     {% for p in info %}
     <div class="post" id="{{ p.floor }}">
-      <img class="avatar" src="/proxy/avatar/{{ p.user.portrait }}">
+      <div>
+	<img class="avatar" src="/proxy/avatar/{{ p.user.portrait }}">
+	{% if p.user.is_bawu %}
+	<div style="background-color: var(--important-color); !important" class="tier">吧务</div>
+	{% endif %}
+	<div class="tier">level {{ p.user.level }}</div>
+      </div>
       <div>
 	<div class="userinfo">
 	  <a href="/home/main?id={{ p.user.user_id }}">{{ p.user.user_name }}</a>
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
-- 
GitLab