From 9df44d5a722e71d48099c2e14f6ce02cf858702e Mon Sep 17 00:00:00 2001
From: John Xina <bingchilling@riseup.net>
Date: Wed, 12 Jul 2023 22:52:47 +0800
Subject: [PATCH] add several features

---
 app.py                | 82 +++++++++++++++++++++++++++++++++----------
 extra.py              |  6 +++-
 main.py               | 11 +++---
 shared.py             |  8 ++++-
 static/css/main.css   | 15 ++++++++
 templates/bar.html    | 23 +++++++++---
 templates/thread.html | 18 ++++++++++
 7 files changed, 131 insertions(+), 32 deletions(-)

diff --git a/app.py b/app.py
index 1e3a7aa..28f89b5 100644
--- a/app.py
+++ b/app.py
@@ -2,7 +2,6 @@ import asyncio
 import aiotieba
 
 from aioflask import render_template, request, escape
-from flask_caching import Cache
 from urllib.parse import quote_plus
 from datetime import datetime
 
@@ -14,6 +13,21 @@ from extra import *
 
 ######################################################################
 
+# Clean a leading part and append the text.
+def append_with_leading_clean(orig, content):
+    if orig.endswith('<br>'):
+        return orig[:-4] + content
+    else:
+        return orig + content
+
+# Return the corresponding user name for an id.
+async def cache_name_from_id(c, i):
+    if not cache.get(i):
+        r = await c.get_user_info(i, require=aiotieba.enums.ReqUInfo.USER_NAME)
+        cache.set(i, r)
+
+######################################################################
+
 # Convert a timestamp to its simpliest readable date format.
 @app.template_filter('simpledate')
 def _jinja2_filter_simpledate(ts):
@@ -44,28 +58,44 @@ def _jinja2_filter_trim(text):
 
 # Format fragments to its equiviant HTML.
 @app.template_filter('translate')
-def _jinja2_filter_translate(frags):
+async def _jinja2_filter_translate(frags, reply_id=0):
     htmlfmt = ''
+
+    if reply_id:
+        htmlfmt += f'<a href="/home/main?id={reply_id}">@{ cache.get(reply_id) }</a> '
     
-    for frag in frags:
+    for i in range(len(frags)):
+        frag = frags[i]
         if isinstance(frag, FragText):
             subfrags = frag.text.split('\n')
             for subfrag in subfrags:
-                htmlfmt += '<p>' + str(escape(subfrag)) + '</p>'
+                htmlfmt += str(escape(subfrag)) + '<br>'
         elif isinstance(frag, FragImage_p):
             htmlfmt += \
                 f'<a target="_blank" href="/proxy/pic/{ extract_image_name(frag.origin_src) }">' \
                 f'<img width="{ frag.show_width}" height="{ frag.show_height }" '\
                 f'src="/proxy/pic/{ extract_image_name(frag.src) }"></a>'
         elif isinstance(frag, FragEmoji_p):
-            clear_leading = False
-            if htmlfmt.endswith('</p>'):
-                clear_leading = True
-                htmlfmt = htmlfmt.rstrip('</p>')
-            htmlfmt += f'<img class="emoticons" alt="[{ frag.desc }]" src="/static/emoticons/{ quote_plus(frag.desc) }.png">'
-            if clear_leading:
-                htmlfmt += '</p>'
-            
+            htmlfmt = append_with_leading_clean(htmlfmt,
+                                                f'<img class="emoticons" alt="[{ frag.desc }]"'
+                                                f'src="/static/emoticons/{ quote_plus(frag.desc) }.png">')
+            if i+1 < len(frags) and isinstance(frags[i+1], FragImage_p):
+                htmlfmt += '<br>'
+        elif isinstance(frag, FragLink):
+            markup = '<a '; url = frag.raw_url
+            if frag.is_external:
+                markup += 'style="text-color: #ff0000;" '
+            else:
+                url = frag.raw_url.lstrip('https://tieba.baidu.com')
+            markup += f'href="{ url }">{ frag.title }</a>'
+            htmlfmt = append_with_leading_clean(htmlfmt, markup)
+        elif isinstance(frag, FragAt):
+            htmlfmt = append_with_leading_clean(htmlfmt,
+                                                f'<a href="/home/main?id={ frag.user_id }">{ frag.text }</a>')
+        else:
+            print('Unhandled: ', type(frag))
+            print(frag)
+
     return htmlfmt
 
 ######################################################################
@@ -77,23 +107,39 @@ async def thread_view(tid):
 
     async with aiotieba.Client() as tieba:
         # Default to 15 posts per page, confirm to tieba.baidu.com
-        thread_info = await tieba.get_posts(tid, rn=15, pn=pn)
+        thread_info = await tieba.get_posts(tid, rn=15, pn=pn,
+                                            with_comments=should_fetch_comments)
+
+        available_users = []
+        for floor in thread_info:
+            for comment in floor.comments:
+                available_users.append(comment.author_id)
+                cache.set(comment.author_id, comment.user.user_name)
+        
+        all_users = {}
+        for floor in thread_info:
+            for comment in floor.comments:
+                if 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))
 
-    for post in thread_info:
-        print(post.comments)
-    
+            
     return await render_template('thread.html', info=thread_info)
 
 @app.route('/f')
 async def forum_view():
     fname =  request.args['kw']
     pn = int(request.args.get('pn') or 1)
+    sort = int(request.args.get('sort') or 0)
 
     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))
+                                                   tieba.get_threads(fname, rn=50, pn=pn, sort=sort))
 
-    return await render_template('bar.html', info=forum_info, threads=threads)
+    return await render_template('bar.html', info=forum_info, threads=threads, sort=sort)
 
 if __name__ == '__main__':
     app.run(debug=True)
diff --git a/extra.py b/extra.py
index 2bfa4e1..9cd533b 100644
--- a/extra.py
+++ b/extra.py
@@ -6,9 +6,13 @@ import re
 
 from shared import *
 
+# TODO: known bug, can't extract from super old editor images.
 def extract_image_name(url):
     match = re.search(r'/(\w+)\.jpg', url)
-    return match.group(1) + '.jpg'
+    try:
+        return match.group(1) + '.jpg'
+    except:
+        return '404.jpg'
 
 @cache.cached(timeout=60, key_prefix='tieba_info')
 def find_tieba_info(tname):
diff --git a/main.py b/main.py
index a02b03c..aaa0056 100644
--- a/main.py
+++ b/main.py
@@ -138,11 +138,12 @@ class ReverseProxyResource(Resource):
 
 # To start this function for testing: python -c 'import main; main.twisted_start()'
 def twisted_start():
-    flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
+    flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+    flask_res = proxy.ReverseProxyResource('127.0.0.1', flask_port, b'')
     flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
     flask_res.putChild(b'static', File('static'))
 
-    flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+    print(f' *** SERVER IS RUNNING ON PORT {flask_port-1} ***')
     
     site = server.Site(flask_res)
     reactor.listenTCP(flask_port-1, site)
@@ -154,11 +155,7 @@ def flask_start():
 
 # If we're executed directly, also start the flask daemon.
 if __name__ == '__main__':
-    flask_port = int(app.config['SERVER_NAME'].split(':')[1])
-    print(f' *** SERVER IS RUNNING ON PORT {flask_port-1} ***')
-    
-    twisted_start()
-    
     flask_task = multiprocessing.Process(target=flask_start)
     flask_task.daemon = True # Exit the child if the parent was killed :-(
     flask_task.start()
+    twisted_start()
diff --git a/shared.py b/shared.py
index 87adbef..f0b0f98 100644
--- a/shared.py
+++ b/shared.py
@@ -12,7 +12,13 @@ def awaitify(sync_func):
 
 app = Flask(__name__)
 
-app.config['SERVER_NAME'] = ':6666'
+######################################################################
+
+app.config['SERVER_NAME'] = '127.0.0.1:8886'
+
+should_fetch_comments = True
+
+######################################################################
 
 app.config['CACHE_TYPE'] = 'SimpleCache'
 cache = Cache(app)
diff --git a/static/css/main.css b/static/css/main.css
index a85c19a..6b188a2 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -2,6 +2,21 @@
     max-width: 5% !important;
 }
 
+.vlist {
+    display: flex;
+    justify-content: space-between;
+}
+
+.vlist > div {
+    flex: 1;
+    text-align: center;
+    margin: 0.5em;
+}
+
+.current-sel {
+    color: inherit !important;
+}
+
 /* global styling */
 :root {
 	--bg-color: #eeeecc;
diff --git a/templates/bar.html b/templates/bar.html
index 73c38c2..ae3e047 100644
--- a/templates/bar.html
+++ b/templates/bar.html
@@ -21,6 +21,16 @@
   </div>
 </div>
 </header>
+<div class="list">
+  <div class="vlist">
+    <div><a {% if sort == 0 %} class="current-sel" {% endif %}
+	    href="/f?kw={{ info['name'] }}&pn={{ threads.page.current_page }}&sort=0">时下热门</a></div>
+    <div><a {% if sort == 1 %} class="current-sel" {% endif %}
+	    href="/f?kw={{ info['name'] }}&pn={{ threads.page.current_page }}&sort=1">最新发布</a></div>
+    <div><a {% if sort == 5 %} class="current-sel" {% endif %}
+	    href="/f?kw={{ info['name'] }}&pn={{ threads.page.current_page }}&sort=5">最新回复</a></div>
+  </div>
+</div>
 <div class="list">
   {% for t in threads %}
   <div class="thread">
@@ -34,9 +44,12 @@
 	%}<span class="tag tag-blue">置顶</span>{%
 	endif %}{% if t.is_good
 	%}<span class="tag tag-red">精</span>{% endif
-	%}<a href="/p/{{ t.tid }}">{{ t.title }} </a>
+	%}<a href="/p/{{ t.tid }}">{{ t.title if t.title else t.text|trim }} </a>
       </div>
+
+      {% if t.title %}
       <div>{{ t.text[(t.title|length):]|trim }}</div>
+      {% endif %}
     </div>
     <div class="participants">
       <div>🧑<a href="">{{ t.user.user_name }}</a></div>
@@ -47,13 +60,13 @@
 
   <div class="paginator">
     {% if threads.page.current_page > 1 %}
-    <a href="/f?kw={{ info['name'] }}">首页</a>
+    <a href="/f?kw={{ info['name'] }}&sort={{ sort }}">首页</a>
     {% endif %}
 
     {% for i in range(5) %}
     {% set np = threads.page.current_page - 5 + i %}
     {% if np > 0 %}
-    <a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
+    <a href="/f?kw={{ info['name'] }}&pn={{ np }}&sort={{ sort }}">{{ np }}</a>
     {% endif %}
     {% endfor %}
 
@@ -62,12 +75,12 @@
     {% for i in range(5) %}
     {% set np = threads.page.current_page + 1 + i %}
     {% if np <= threads.page.total_page %}
-    <a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
+    <a href="/f?kw={{ info['name'] }}&pn={{ np }}&sort={{ sort }}">{{ np }}</a>
     {% endif %}
     {% endfor %}
 
     {% if threads.page.current_page < threads.page.total_page %}
-    <a href="/f?kw={{ info['name'] }}&pn={{ threads.page.total_page }}">尾页</a>
+    <a href="/f?kw={{ info['name'] }}&pn={{ threads.page.total_page }}&sort={{ sort }}">尾页</a>
     {% endif %}
   </div>
 </div>
diff --git a/templates/thread.html b/templates/thread.html
index 2e5be92..fc06ed5 100644
--- a/templates/thread.html
+++ b/templates/thread.html
@@ -30,6 +30,24 @@
 	</div>
 	<small class="date">{{ p.create_time|date }}</small>
 	<small class="permalink"><a href="#{{ p.floor }}">{{ p.floor }}</a></small>
+	{% if p.comments %}
+	<div class="replies">
+	  {% for comment in p.comments %}
+	  <div class="post">
+	    <img class="avatar" src="/proxy/avatar/{{ comment.user.portrait }}">
+	    <div>
+	      <div class="userinfo">
+		<a href="/home/main?id={{ comment.user.user_id }}">{{ comment.user.user_name }}</a>
+	      </div>
+	      <div class="content">
+		{{ comment.contents|translate(comment.reply_to_id)|safe }}
+	      </div>
+	      <small class="date">{{ comment.create_time|date }}</small>
+	    </div>
+	  </div>
+	  {% endfor %}
+	</div>
+	{% endif %}
       </div>
     </div>
     {% endfor %}
-- 
GitLab