2023-07-11 11:32:13 +00:00
|
|
|
|
import asyncio
|
|
|
|
|
import aiotieba
|
2023-07-15 12:01:35 +00:00
|
|
|
|
import uvicorn
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
import re
|
|
|
|
|
import textwrap
|
|
|
|
|
|
2023-07-11 14:23:48 +00:00
|
|
|
|
from aioflask import render_template, request, escape
|
|
|
|
|
from urllib.parse import quote_plus
|
2023-07-11 11:32:13 +00:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
2023-07-11 14:23:48 +00:00
|
|
|
|
from aiotieba.api.get_posts._classdef import *
|
|
|
|
|
from aiotieba.api._classdef.contents import *
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
2023-07-15 12:01:35 +00:00
|
|
|
|
from proxify import AsgiproxifyHandler
|
|
|
|
|
|
2023-07-11 14:23:48 +00:00
|
|
|
|
from shared import *
|
|
|
|
|
from extra import *
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2023-07-12 14:52:47 +00:00
|
|
|
|
# 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):
|
2023-07-24 07:00:19 +00:00
|
|
|
|
if not cache.get(str(i)):
|
|
|
|
|
r = await c.get_user_info(i, require=aiotieba.enums.ReqUInfo.NICK_NAME)
|
|
|
|
|
cache.set(str(i), r)
|
2023-07-12 14:52:47 +00:00
|
|
|
|
|
2023-07-15 12:01:35 +00:00
|
|
|
|
# Normalize unicode characters to ASCII form.
|
|
|
|
|
def normalize_utf8(s):
|
|
|
|
|
return s.encode('unicode_escape').decode('ascii').replace('\\', '')
|
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
# Render the template with compatibility detection.
|
|
|
|
|
def render_template_c(tmpl, **kwargs):
|
2023-07-29 01:17:37 +00:00
|
|
|
|
text_browsers = ['w3m', 'Lynx', 'ELinks', 'Links', 'URL/Emacs', 'Emacs',
|
|
|
|
|
'Mozilla/5.0 (compatible; hjdicks)', # abaco, mothra, etc
|
|
|
|
|
|
|
|
|
|
]
|
2023-07-24 06:45:05 +00:00
|
|
|
|
ua = request.headers.get('User-Agent')
|
|
|
|
|
|
|
|
|
|
for text_ua in text_browsers:
|
|
|
|
|
if ua.startswith(text_ua):
|
|
|
|
|
return render_template(f'{tmpl}.text', **kwargs)
|
|
|
|
|
return render_template(tmpl, **kwargs)
|
|
|
|
|
|
|
|
|
|
# Wrap the text to the given width.
|
|
|
|
|
def wrap_text(i, width=70, join='\n', rws=False):
|
|
|
|
|
i = str(i)
|
|
|
|
|
def add_whitespace_to_chinese(match):
|
|
|
|
|
return '\uFFFD' + match.group(0)
|
|
|
|
|
pattern = r'[\u4e00-\u9fff\uFF00-\uFFEF]'
|
|
|
|
|
aft = re.sub(pattern, add_whitespace_to_chinese, i)
|
|
|
|
|
return join.join(textwrap.wrap(aft, width=width, replace_whitespace=rws)).replace('\uFFFD', '')
|
|
|
|
|
|
2023-07-12 14:52:47 +00:00
|
|
|
|
######################################################################
|
|
|
|
|
|
2023-07-11 11:32:13 +00:00
|
|
|
|
# Convert a timestamp to its simpliest readable date format.
|
|
|
|
|
@app.template_filter('simpledate')
|
|
|
|
|
def _jinja2_filter_simpledate(ts):
|
|
|
|
|
t = datetime.fromtimestamp(ts)
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
|
|
|
|
if t.date() == now.date():
|
|
|
|
|
return t.strftime('%H:%m')
|
|
|
|
|
elif t.year == now.year:
|
|
|
|
|
return t.strftime('%m-%d')
|
|
|
|
|
else:
|
|
|
|
|
return t.strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
|
|
# Convert a timestamp to a humand readable date format.
|
|
|
|
|
@app.template_filter('date')
|
|
|
|
|
def _jinja2_filter_datetime(ts, fmt='%Y年%m月%d日 %H点%m分'):
|
|
|
|
|
return datetime.fromtimestamp(ts).strftime(fmt)
|
|
|
|
|
|
|
|
|
|
# Convert a integer to the one with separator like 1,000,000.
|
|
|
|
|
@app.template_filter('intsep')
|
|
|
|
|
def _jinja2_filter_intsep(i):
|
|
|
|
|
return f'{int(i):,}'
|
|
|
|
|
|
|
|
|
|
# Reduce the text to a shorter form.
|
|
|
|
|
@app.template_filter('trim')
|
|
|
|
|
def _jinja2_filter_trim(text):
|
|
|
|
|
return text[:78] + '……' if len(text) > 78 else text
|
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
# Format comments to its equiviant text HTML.
|
|
|
|
|
@app.template_filter('tcomments')
|
|
|
|
|
async def _jinja2_filter_tcomments(coms):
|
|
|
|
|
buf = ' | '
|
|
|
|
|
for com in coms:
|
|
|
|
|
buf += wrap_text(f'{ com.user.show_name }:{ com.text }', width=60, join="\n | ")
|
|
|
|
|
buf += '\n \---\n | '
|
|
|
|
|
return buf[:-4]
|
|
|
|
|
|
2023-07-11 14:23:48 +00:00
|
|
|
|
# Format fragments to its equiviant HTML.
|
|
|
|
|
@app.template_filter('translate')
|
2023-07-12 14:52:47 +00:00
|
|
|
|
async def _jinja2_filter_translate(frags, reply_id=0):
|
2023-07-11 14:23:48 +00:00
|
|
|
|
htmlfmt = ''
|
2023-07-12 14:52:47 +00:00
|
|
|
|
|
|
|
|
|
if reply_id:
|
2023-07-24 07:00:19 +00:00
|
|
|
|
htmlfmt += f'<a href="/home/main?id={reply_id}">@{ cache.get(str(reply_id)) }</a> '
|
2023-07-11 14:23:48 +00:00
|
|
|
|
|
2023-07-12 14:52:47 +00:00
|
|
|
|
for i in range(len(frags)):
|
|
|
|
|
frag = frags[i]
|
2023-07-11 14:23:48 +00:00
|
|
|
|
if isinstance(frag, FragText):
|
|
|
|
|
subfrags = frag.text.split('\n')
|
|
|
|
|
for subfrag in subfrags:
|
2023-07-12 14:52:47 +00:00
|
|
|
|
htmlfmt += str(escape(subfrag)) + '<br>'
|
2023-07-11 14:23:48 +00:00
|
|
|
|
elif isinstance(frag, FragImage_p):
|
2023-07-27 02:16:33 +00:00
|
|
|
|
# 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>'
|
|
|
|
|
|
|
|
|
|
# Leah Rowe's lightbox implementation
|
2023-07-11 14:23:48 +00:00
|
|
|
|
htmlfmt += \
|
2023-07-27 02:32:09 +00:00
|
|
|
|
f'<img tabindex=1 width="{ frag.show_width }" height="{ frag.show_height }" src="/proxy/pic/{ extract_image_name(frag.src) }" />' \
|
|
|
|
|
f'<span class="f"><img src="/proxy/pic/{ extract_image_name(frag.origin_src) }" /></span>'
|
2023-07-11 14:23:48 +00:00
|
|
|
|
elif isinstance(frag, FragEmoji_p):
|
2023-07-12 14:52:47 +00:00
|
|
|
|
htmlfmt = append_with_leading_clean(htmlfmt,
|
|
|
|
|
f'<img class="emoticons" alt="[{ frag.desc }]"'
|
2023-07-15 12:01:35 +00:00
|
|
|
|
f'src="/static/emoticons/{ normalize_utf8(frag.desc) }.png">')
|
2023-07-12 14:52:47 +00:00
|
|
|
|
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:
|
2024-02-19 14:10:48 +00:00
|
|
|
|
url = frag.raw_url.path
|
2023-07-12 14:52:47 +00:00
|
|
|
|
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)
|
|
|
|
|
|
2023-07-11 14:23:48 +00:00
|
|
|
|
return htmlfmt
|
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
@app.template_filter('twrap')
|
|
|
|
|
async def _jinja2_filter_translate(text, width=70, join='\n', rws=False):
|
|
|
|
|
return wrap_text(text, width, join, rws)
|
|
|
|
|
|
2023-07-11 11:32:13 +00:00
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
@app.route('/p/<tid>')
|
|
|
|
|
async def thread_view(tid):
|
|
|
|
|
tid = int(tid)
|
|
|
|
|
pn = int(request.args.get('pn') or 1)
|
2023-07-13 12:06:44 +00:00
|
|
|
|
ao = int(request.args.get('ao') or 0)
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
|
|
|
|
async with aiotieba.Client() as tieba:
|
|
|
|
|
# Default to 15 posts per page, confirm to tieba.baidu.com
|
2023-07-12 14:52:47 +00:00
|
|
|
|
thread_info = await tieba.get_posts(tid, rn=15, pn=pn,
|
2023-07-13 12:06:44 +00:00
|
|
|
|
with_comments=should_fetch_comments,
|
|
|
|
|
only_thread_author=ao)
|
2023-07-12 14:52:47 +00:00
|
|
|
|
|
2024-02-19 14:10:48 +00:00
|
|
|
|
if thread_info.err:
|
|
|
|
|
return await runtime_error_view(thread_info.err)
|
|
|
|
|
|
2023-07-12 14:52:47 +00:00
|
|
|
|
available_users = []
|
|
|
|
|
for floor in thread_info:
|
|
|
|
|
for comment in floor.comments:
|
|
|
|
|
available_users.append(comment.author_id)
|
2023-07-24 07:00:19 +00:00
|
|
|
|
cache.set(str(comment.author_id), comment.user.show_name)
|
2023-07-12 14:52:47 +00:00
|
|
|
|
|
|
|
|
|
all_users = {}
|
|
|
|
|
for floor in thread_info:
|
|
|
|
|
for comment in floor.comments:
|
2023-07-13 10:48:49 +00:00
|
|
|
|
if comment.reply_to_id and not comment.reply_to_id in available_users:
|
2023-07-12 14:52:47 +00:00
|
|
|
|
all_users[comment.reply_to_id] = ''
|
|
|
|
|
all_users = list(all_users.keys())
|
|
|
|
|
|
|
|
|
|
await asyncio.gather(*(cache_name_from_id(tieba, i) for i in all_users))
|
2023-07-13 10:48:49 +00:00
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('thread.html', info=thread_info, ao=ao)
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
|
|
|
|
@app.route('/f')
|
|
|
|
|
async def forum_view():
|
2023-07-13 13:09:04 +00:00
|
|
|
|
fname = request.args['kw'][:-1] if request.args['kw'][-1] == '吧' else request.args['kw']
|
2023-07-11 11:32:13 +00:00
|
|
|
|
pn = int(request.args.get('pn') or 1)
|
2023-07-13 12:24:28 +00:00
|
|
|
|
sort = int(request.args.get('sort') or 0)
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
|
|
|
|
async with aiotieba.Client() as tieba:
|
2023-07-15 12:01:35 +00:00
|
|
|
|
forum_info, threads = await asyncio.gather(tieba.get_forum_detail(fname),
|
|
|
|
|
tieba.get_threads(fname, pn=pn, sort=sort))
|
2024-02-19 14:10:48 +00:00
|
|
|
|
if threads.err:
|
|
|
|
|
return await runtime_error_view(threads.err)
|
|
|
|
|
elif forum_info.err:
|
|
|
|
|
return await runtime_error_view(forum_info.err)
|
|
|
|
|
|
|
|
|
|
|
2023-07-15 12:01:35 +00:00
|
|
|
|
if hasattr(forum_info, 'slogan'):
|
|
|
|
|
forum_info = { 'avatar': extract_image_name(forum_info.origin_avatar),
|
|
|
|
|
'topic': forum_info.post_num, 'thread': forum_info.post_num,
|
|
|
|
|
'member': forum_info.member_num, 'desc': forum_info.slogan,
|
|
|
|
|
'name': forum_info.fname }
|
2023-07-14 11:10:45 +00:00
|
|
|
|
else:
|
2023-07-15 12:01:35 +00:00
|
|
|
|
forum_info = { 'avatar': 'a6efce1b9d16fdfa6291460ab98f8c5495ee7b51.jpg',
|
|
|
|
|
'topic': forum_info.post_num, 'thread': forum_info.post_num,
|
|
|
|
|
'member': forum_info.member_num, 'desc': '贴吧描述暂不可用', 'name': forum_info.fname }
|
2023-07-14 11:10:45 +00:00
|
|
|
|
|
2023-07-13 10:48:49 +00:00
|
|
|
|
if threads.page.current_page > threads.page.total_page or pn < 1:
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('error.html', msg = \
|
2023-07-13 10:48:49 +00:00
|
|
|
|
f'请求越界,本贴吧共有 { threads.page.total_page } 页'
|
|
|
|
|
f'而您查询了第 { threads.page.current_page} 页')
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('bar.html', info=forum_info, threads=threads, sort=sort,
|
2023-07-13 12:24:28 +00:00
|
|
|
|
tp = ((115 if threads.page.total_page > 115 else threads.page.total_page) if sort == 0 else threads.page.total_page))
|
2023-07-11 11:32:13 +00:00
|
|
|
|
|
2023-07-13 10:48:49 +00:00
|
|
|
|
@app.route('/home/main')
|
|
|
|
|
async def user_view():
|
2023-07-14 01:54:05 +00:00
|
|
|
|
pn = int(request.args.get('pn') or 1)
|
|
|
|
|
i = request.args.get('id')
|
|
|
|
|
try: # try converting it to user_id, otherwise using the string.
|
|
|
|
|
i = int(i)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async with aiotieba.Client() as tieba:
|
|
|
|
|
try:
|
|
|
|
|
hp = await tieba.get_homepage(i, pn)
|
2024-02-19 14:10:48 +00:00
|
|
|
|
if hp.err:
|
|
|
|
|
return await runtime_error_view(hp.err)
|
2023-07-14 01:54:05 +00:00
|
|
|
|
except ValueError:
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('error.html', msg='您已超过最后页')
|
2023-07-14 01:54:05 +00:00
|
|
|
|
|
2024-02-19 14:10:48 +00:00
|
|
|
|
if not hp.objs and pn > 1:
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('error.html', msg='您已超过最后页')
|
2023-07-14 01:54:05 +00:00
|
|
|
|
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('user.html', hp=hp, pn=pn)
|
2023-07-13 10:48:49 +00:00
|
|
|
|
|
2023-07-13 13:09:04 +00:00
|
|
|
|
@app.route('/')
|
|
|
|
|
async def main_view():
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('index.html')
|
2023-07-13 13:09:04 +00:00
|
|
|
|
|
2023-07-13 10:48:49 +00:00
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
@app.errorhandler(RuntimeError)
|
|
|
|
|
async def runtime_error_view(e):
|
2023-07-14 01:54:05 +00:00
|
|
|
|
if hasattr(e, 'msg'):
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('error.html', msg=e.msg)
|
2024-02-19 14:10:48 +00:00
|
|
|
|
return await render_template_c('error.html', msg=str(e) or '错误信息不可用')
|
2023-07-13 10:48:49 +00:00
|
|
|
|
|
|
|
|
|
@app.errorhandler(Exception)
|
|
|
|
|
async def general_error_view(e):
|
2023-07-24 06:45:05 +00:00
|
|
|
|
return await render_template_c('error.html', msg=e)
|
2023-07-13 10:48:49 +00:00
|
|
|
|
|
2023-07-15 12:01:35 +00:00
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
@proxified.register('/proxy/avatar/')
|
|
|
|
|
class AvatarProxyHandler(AsgiproxifyHandler):
|
|
|
|
|
def make_request_url(self):
|
2024-01-17 02:37:30 +00:00
|
|
|
|
return 'http://himg.baidu.com/sys/portraith/item/' + self.scope['path'][14:]
|
2023-07-15 12:01:35 +00:00
|
|
|
|
|
|
|
|
|
@proxified.register('/proxy/pic/')
|
|
|
|
|
class PictureProxyHandler(AsgiproxifyHandler):
|
|
|
|
|
def make_request_url(self):
|
|
|
|
|
return 'http://imgsa.baidu.com/forum/pic/item/' + self.scope['path'][11:]
|
|
|
|
|
|
2023-07-11 11:32:13 +00:00
|
|
|
|
if __name__ == '__main__':
|
2023-07-15 12:01:35 +00:00
|
|
|
|
uvicorn.run(proxified, host=host, port=port)
|