mirror of
https://0xacab.org/johnxina/rat.git
synced 2024-12-23 13:09:08 +00:00
minimal
This commit is contained in:
parent
aafa24ddc7
commit
f17e332ee4
42
app.py
42
app.py
@ -1,12 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import aiotieba
|
import aiotieba
|
||||||
|
|
||||||
from aioflask import Flask, render_template, request
|
from aioflask import render_template, request, escape
|
||||||
|
from flask_caching import Cache
|
||||||
|
from urllib.parse import quote_plus
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from extra import *
|
from aiotieba.api.get_posts._classdef import *
|
||||||
|
from aiotieba.api._classdef.contents import *
|
||||||
|
|
||||||
app = Flask(__name__)
|
from shared import *
|
||||||
|
from extra import *
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@ -38,6 +42,32 @@ def _jinja2_filter_intsep(i):
|
|||||||
def _jinja2_filter_trim(text):
|
def _jinja2_filter_trim(text):
|
||||||
return text[:78] + '……' if len(text) > 78 else text
|
return text[:78] + '……' if len(text) > 78 else text
|
||||||
|
|
||||||
|
# Format fragments to its equiviant HTML.
|
||||||
|
@app.template_filter('translate')
|
||||||
|
def _jinja2_filter_translate(frags):
|
||||||
|
htmlfmt = ''
|
||||||
|
|
||||||
|
for frag in frags:
|
||||||
|
if isinstance(frag, FragText):
|
||||||
|
subfrags = frag.text.split('\n')
|
||||||
|
for subfrag in subfrags:
|
||||||
|
htmlfmt += '<p>' + str(escape(subfrag)) + '</p>'
|
||||||
|
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>'
|
||||||
|
|
||||||
|
return htmlfmt
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@app.route('/p/<tid>')
|
@app.route('/p/<tid>')
|
||||||
@ -49,8 +79,8 @@ async def thread_view(tid):
|
|||||||
# Default to 15 posts per page, confirm to tieba.baidu.com
|
# 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)
|
||||||
|
|
||||||
for fragment in thread_info[0].contents:
|
for post in thread_info:
|
||||||
print(fragment)
|
print(post.comments)
|
||||||
|
|
||||||
return await render_template('thread.html', info=thread_info)
|
return await render_template('thread.html', info=thread_info)
|
||||||
|
|
||||||
@ -60,7 +90,7 @@ async def forum_view():
|
|||||||
pn = int(request.args.get('pn') or 1)
|
pn = int(request.args.get('pn') or 1)
|
||||||
|
|
||||||
async with aiotieba.Client() as tieba:
|
async with aiotieba.Client() as tieba:
|
||||||
forum_info, threads = await asyncio.gather(find_tieba_info(fname),
|
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))
|
||||||
|
|
||||||
return await render_template('bar.html', info=forum_info, threads=threads)
|
return await render_template('bar.html', info=forum_info, threads=threads)
|
||||||
|
13
extra.py
13
extra.py
@ -4,7 +4,14 @@ import requests
|
|||||||
import bs4
|
import bs4
|
||||||
import re
|
import re
|
||||||
|
|
||||||
async def find_tieba_info(tname):
|
from shared import *
|
||||||
|
|
||||||
|
def extract_image_name(url):
|
||||||
|
match = re.search(r'/(\w+)\.jpg', url)
|
||||||
|
return match.group(1) + '.jpg'
|
||||||
|
|
||||||
|
@cache.cached(timeout=60, key_prefix='tieba_info')
|
||||||
|
def find_tieba_info(tname):
|
||||||
"""Get the tiebat avatar for the forum name.
|
"""Get the tiebat avatar for the forum name.
|
||||||
|
|
||||||
:param tname: the name of the target forum.
|
:param tname: the name of the target forum.
|
||||||
@ -17,9 +24,7 @@ async def find_tieba_info(tname):
|
|||||||
soup = bs4.BeautifulSoup(res.text, 'html.parser')
|
soup = bs4.BeautifulSoup(res.text, 'html.parser')
|
||||||
|
|
||||||
elems = soup.select('#forum-card-head')
|
elems = soup.select('#forum-card-head')
|
||||||
match = re.search(r'/(\w+)\.jpg', elems[0]['src'])
|
info['avatar'] = extract_image_name(elems[0]['src'])
|
||||||
|
|
||||||
info['avatar'] = match.group(1) + '.jpg'
|
|
||||||
|
|
||||||
footer = soup.select('.th_footer_l')[0]
|
footer = soup.select('.th_footer_l')[0]
|
||||||
stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False)
|
stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False)
|
||||||
|
14
main.py
14
main.py
@ -6,6 +6,7 @@ from urllib.parse import quote as urlquote, urlparse, urlunparse
|
|||||||
from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
|
from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
from twisted.web import proxy, server
|
from twisted.web import proxy, server
|
||||||
|
from twisted.web.static import File
|
||||||
from twisted.internet.protocol import ClientFactory
|
from twisted.internet.protocol import ClientFactory
|
||||||
from twisted.internet import reactor, utils
|
from twisted.internet import reactor, utils
|
||||||
|
|
||||||
@ -139,18 +140,25 @@ class ReverseProxyResource(Resource):
|
|||||||
def twisted_start():
|
def twisted_start():
|
||||||
flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
|
flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
|
||||||
flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
|
flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
|
||||||
|
flask_res.putChild(b'static', File('static'))
|
||||||
|
|
||||||
|
flask_port = int(app.config['SERVER_NAME'].split(':')[1])
|
||||||
|
|
||||||
site = server.Site(flask_res)
|
site = server.Site(flask_res)
|
||||||
reactor.listenTCP(5001, site)
|
reactor.listenTCP(flask_port-1, site)
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
# To start this function for testing: python -c 'import main; main.flask_start()'
|
# To start this function for testing: python -c 'import main; main.flask_start()'
|
||||||
def flask_start():
|
def flask_start():
|
||||||
app.run(port=5000+1)
|
app.run()
|
||||||
|
|
||||||
# If we're executed directly, also start the flask daemon.
|
# If we're executed directly, also start the flask daemon.
|
||||||
if __name__ == '__main__':
|
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 = multiprocessing.Process(target=flask_start)
|
||||||
flask_task.daemon = True # Exit the child if the parent was killed :-(
|
flask_task.daemon = True # Exit the child if the parent was killed :-(
|
||||||
flask_task.start()
|
flask_task.start()
|
||||||
twisted_start()
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
aioflask==0.4.0
|
aioflask==0.4.0
|
||||||
flask==2.1.3
|
flask==2.1.3
|
||||||
aiotieba==3.4.5
|
aiotieba==3.4.5
|
||||||
|
Flask-Caching
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
requests
|
requests
|
||||||
|
twisted
|
||||||
|
18
shared.py
Normal file
18
shared.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from aioflask import Flask
|
||||||
|
from flask_caching import Cache
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
def awaitify(sync_func):
|
||||||
|
"""Wrap a synchronous callable to allow ``await``'ing it"""
|
||||||
|
@wraps(sync_func)
|
||||||
|
async def async_func(*args, **kwargs):
|
||||||
|
return sync_func(*args, **kwargs)
|
||||||
|
return async_func
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.config['SERVER_NAME'] = ':6666'
|
||||||
|
|
||||||
|
app.config['CACHE_TYPE'] = 'SimpleCache'
|
||||||
|
cache = Cache(app)
|
@ -1,19 +1,25 @@
|
|||||||
|
.emoticons {
|
||||||
|
max-width: 5% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* global styling */
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #eeeecc;
|
--bg-color: #eeeecc;
|
||||||
--fg-color: #ffffdd;
|
--fg-color: #ffffdd;
|
||||||
--replies-color: #f0f0d0;
|
--replies-color: #f0f0d0;
|
||||||
--text-color: #663333;
|
--text-color: #663333;
|
||||||
|
--border-color: #66333388;
|
||||||
--primary-color: #0066cc;
|
--primary-color: #0066cc;
|
||||||
--important-color: #ff0000;
|
--important-color: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #000033;
|
--bg-color: #000033;
|
||||||
--fg-color: #202044;
|
--fg-color: #202044;
|
||||||
--replies-color: #16163a;
|
--replies-color: #16163a;
|
||||||
--text-color: #cccccc;
|
--text-color: #cccccc;
|
||||||
|
--border-color: #cccccc44;
|
||||||
--primary-color: #6699ff;
|
--primary-color: #6699ff;
|
||||||
--important-color: #ff0000;
|
--important-color: #ff0000;
|
||||||
}
|
}
|
||||||
@ -27,6 +33,15 @@ body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
a[href] {
|
a[href] {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -40,7 +55,24 @@ img {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-nav, .thread-nav {
|
.paginator {
|
||||||
|
padding: 1rem;
|
||||||
|
gap: .3rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginator a {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
padding: .5rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--replies-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
background-color: var(--fg-color);
|
background-color: var(--fg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -50,38 +82,29 @@ img {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-nav > .title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
flex: 1 0 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thread-nav > .from {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-nav > img {
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-nav .title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-nav .stats small {
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
background-color: var(--fg-color);
|
background-color: var(--fg-color);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* thread.html: nav bar */
|
||||||
|
|
||||||
|
.thread-nav .title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 1 0 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-nav .from {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* thread.html: user post */
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 1rem;
|
gap: 0 1rem;
|
||||||
border-bottom: 1px solid var(--text-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +143,39 @@ img {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* thread.html: replies to a user post */
|
||||||
|
|
||||||
|
.post .replies {
|
||||||
|
background-color: var(--replies-color);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .replies .post {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .replies .post .avatar {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bar.html: nav bar */
|
||||||
|
|
||||||
|
.bar-nav img {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-nav .title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-nav .stats small {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bar.html: thread list */
|
||||||
|
|
||||||
.thread {
|
.thread {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@ -155,12 +211,3 @@ img {
|
|||||||
padding-left: .3rem;
|
padding-left: .3rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
padding: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
@ -44,6 +44,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="paginator">
|
||||||
|
{% if threads.page.current_page > 1 %}
|
||||||
|
<a href="/f?kw={{ info['name'] }}">首页</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>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<a>{{ threads.page.current_page }}</a>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if threads.page.current_page < threads.page.total_page %}
|
||||||
|
<a href="/f?kw={{ info['name'] }}&pn={{ threads.page.total_page }}">尾页</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div><a href="#">RAT Ain't Tieba</a></div>
|
<div><a href="#">RAT Ain't Tieba</a></div>
|
||||||
|
@ -26,14 +26,41 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ p['text'] }}
|
{{ p.contents|translate|safe }}
|
||||||
</div>
|
</div>
|
||||||
<small class="date">{{ p.create_time|date }}</small>
|
<small class="date">{{ p.create_time|date }}</small>
|
||||||
<small class="permalink"><a href="#1">{{ p.floor }}</a></small>
|
<small class="permalink"><a href="#{{ p.floor }}">{{ p.floor }}</a></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="paginator">
|
||||||
|
{% if info.page.current_page > 1 %}
|
||||||
|
<a href="/p/{{ info.thread.tid }}">首页</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for i in range(5) %}
|
||||||
|
{% set np = info.page.current_page - 5 + i %}
|
||||||
|
{% if np > 0 %}
|
||||||
|
<a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<a>{{ info.page.current_page }}</a>
|
||||||
|
|
||||||
|
{% for i in range(5) %}
|
||||||
|
{% set np = info.page.current_page + 1 + i %}
|
||||||
|
{% if np <= info.page.total_page %}
|
||||||
|
<a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if info.page.current_page < info.page.total_page %}
|
||||||
|
<a href="/p/{{ info.thread.tid }}?pn={{ info.page.total_page }}">尾页</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div><a href="/">RAT Ain't Tieba</a></div>
|
<div><a href="/">RAT Ain't Tieba</a></div>
|
||||||
<div><a href="#">自豪地以 AGPLv3 释出</a></div>
|
<div><a href="#">自豪地以 AGPLv3 释出</a></div>
|
||||||
|
Loading…
Reference in New Issue
Block a user