Browse Source

允许搜索

master
hole-thu 4 years ago
parent
commit
24947c9874
  1. 19
      config.sample.py
  2. 361
      hole.py
  3. 9
      models.py
  4. 14
      requirements.txt
  5. 81
      utils.py

19
config.sample.py

@ -1,15 +1,14 @@
import random, string, time
import random
import string
import time
SQLALCHEMY_DATABASE_URI='sqlite:///hole.db'
SQLALCHEMY_TRACK_MODIFICATIONS=False
JSON_AS_ASCII=False
CLIENT_ID='<id>'
CLIENT_SECRET='<secret>'
MASTODON_URL='https://thu.closed.social'
SQLALCHEMY_DATABASE_URI = 'sqlite:///hole.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
JSON_AS_ASCII = False
CLIENT_ID = '<id>'
CLIENT_SECRET = '<secret>'
MASTODON_URL = 'https://mastodon.social'
REDIRECT_URI = 'http://hole.thu.monster/_auth'
THUHOLE_ADDRESS='https://thuhole.com'
THUHOLE_HOST='thuhole.com'
THUHOLE_PID=1
SALT = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
ADMINS = ['cs_114514']
START_TIME = int(time.time())

361
hole.py

@ -1,12 +1,13 @@
from flask import Flask, request, render_template, send_from_directory, abort, redirect
from flask_sqlalchemy import SQLAlchemy
import re
import random
import string
from flask import Flask, request, abort, redirect
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_migrate import Migrate
from mastodon import Mastodon
import re, random, string, datetime, hashlib,requests
from models import db, User, Post, Comment, Attention, TagRecord, Syslog
from utils import require_token, map_post, map_comment, map_syslog, check_attention, hash_name, look, get_num, tmp_token
@ -19,18 +20,13 @@ app.config.from_pyfile('config.py')
db.init_app(app)
migrate = Migrate(app, db)
#with app.app_context():
# db.create_all()
CS_LOGIN_URL = Mastodon(api_base_url=app.config['MASTODON_URL']) \
.auth_request_url(
client_id = app.config['CLIENT_ID'],
redirect_uris = app.config['REDIRECT_URI'],
scopes = ['read:accounts']
)
THUHOLE_SEND_URL = f"{app.config.get('THUHOLE_ADDRESS')}/services/thuhole/api.php?action=docomment&PKUHelperAPI=3.0&jsapiver=v0.3.1.133-444340&user_token="
THUHOLE_GET_URL = f"{app.config.get('THUHOLE_ADDRESS')}/services/thuhole/api.php?action=getcomment&pid={app.config.get('THUHOLE_PID')}&PKUHelperAPI=3.0&jsapiver=v0.3.1.133-444340&user_token="
.auth_request_url(
client_id=app.config['CLIENT_ID'],
redirect_uris=app.config['REDIRECT_URI'],
scopes=['read:accounts']
)
limiter = Limiter(
app,
@ -40,77 +36,32 @@ limiter = Limiter(
PER_PAGE = 50
@app.route('/_login')
@limiter.limit("5 / minute, 50 / hour")
def login():
provider = request.args.get('p')
if provider == 'cs':
return redirect(CS_LOGIN_URL)
elif provider == 'thuhole':
token = request.args.get('token')
try:
rt = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
headers = {
'user-agent': 'holeBot; hole.thu.monster',
'host': app.config.get('THUHOLE_HOST')
}
r = requests.post(
THUHOLE_SEND_URL+token,
headers=headers,
data={
'pid': app.config.get('THUHOLE_PID'),
'text': rt,
'user_token': token
}
)
r = requests.get(
THUHOLE_GET_URL+token,
headers=headers
)
c = r.json()
data = c.get('data')
mat = [c['name'] for c in data if c['text'].endswith(rt)]
if mat:
name = mat[0]
else:
abort(401)
name = 'th_' + ''.join(map(lambda s: s[0], name.split()))
u = v = User.query.filter_by(name=name).first()
if not u:
u = User(name=name)
db.session.add(u)
if not v or False: #TODO: reset token
u.token = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
db.session.commit()
return redirect('/?token='+ u.token)
except :
abort(401)
abort(404)
@app.route('/_auth')
@limiter.limit("5 / minute")
def auth():
# Currently, only for closed.social
code = request.args.get('code')
client = Mastodon(
client_id = app.config['CLIENT_ID'],
client_secret = app.config['CLIENT_SECRET'],
api_base_url = app.config['MASTODON_URL']
)
token = client.log_in(
code=code,
redirect_uri=app.config['REDIRECT_URI'],
scopes=['read:accounts']
)
client_id=app.config['CLIENT_ID'],
client_secret=app.config['CLIENT_SECRET'],
api_base_url=app.config['MASTODON_URL']
)
client.log_in(
code=code,
redirect_uri=app.config['REDIRECT_URI'],
scopes=['read:accounts']
)
info = client.account_verify_credentials()
name = 'cs_' + str(info.id)
@ -121,11 +72,16 @@ def auth():
u = User(name=name)
db.session.add(u)
if not v or False: #TODO: reset token
u.token = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
if not v or False: # TODO: reset token
u.token = ''.join(
random.choices(
string.ascii_letters +
string.digits,
k=16))
db.session.commit()
return redirect('/?token='+ u.token)
return redirect('/?token=%s' % u.token)
@app.route('/_api/v1/getlist')
def get_list():
@ -136,54 +92,86 @@ def get_list():
posts = Post.query.filter_by(deleted=False)
if 'no_cw' in request.args:
posts = posts.filter_by(cw=None)
posts = posts.order_by(db.desc('comment_timestamp')) if 'by_c' in request.args else posts.order_by(db.desc('id'))
posts = posts.order_by(
db.desc('comment_timestamp')) if 'by_c' in request.args else posts.order_by(
db.desc('id'))
posts = posts.paginate(p, PER_PAGE)
data =list(map(map_post, posts.items, [u.name] * len(posts.items)))
data = list(map(map_post, posts.items, [u.name] * len(posts.items)))
return {
'code': 0,
'tmp_token': tmp_token(),
'count': len(data),
'data': data
}
'code': 0,
'tmp_token': tmp_token(),
'count': len(data),
'data': data
}
@app.route('/_api/v1/getone')
def get_one():
u = require_token()
pid = get_num(request.args.get('pid'))
pid = request.args.get('pid', type=int)
post = Post.query.get(pid)
if not post: abort(404)
if post.deleted: abort(451)
if not post:
abort(404)
if post.deleted:
abort(451)
data = map_post(post, u.name)
return {
'code': 0,
'data': data
}
'code': 0,
'data': data
}
@app.route('/_api/v1/search')
def search():
u = require_token()
page = get_num(request.args.get('page'))
pagesize = max(get_num(request.args.get('pagesize')), 200)
page = request.args.get('page', type=int, default=1)
pagesize = min(request.args.get('pagesize', type=int, default=200), 200)
keywords = request.args.get('keywords')
if not keywords:
abort(422)
pids = [tr.pid for tr in TagRecord.query.filter_by(tag=keywords).order_by(db.desc('pid')).paginate(page, pagesize).items]
tag_pids = TagRecord.query.with_entities(
TagRecord.pid
).filter_by(
tag=keywords
).all()
data = [ map_post(Post.query.get(pid), u.name)
for pid in pids if Post.query.get(pid) and not Post.query.get(pid).deleted
]
print(tag_pids)
return {
'code': 0,
'count': len(data),
'data': data
}
tag_pids = [tag_pid for tag_pid, in tag_pids] or [0] # sql not allowed empty in
posts = Post.query.filter(
Post.search_text.like("%{}%".format(keywords))
).filter(
Post.id.notin_(tag_pids)
).order_by(
Post.id.desc()
).limit(pagesize).offset((page - 1) * pagesize).all()
if page == 1:
posts = Post.query.filter(
Post.id.in_(tag_pids)
).filter_by(deleted=False).order_by(
Post.id.desc()
).all() + posts
data = [
map_post(post, u.name)
for post in posts
]
return {
'code': 0,
'count': len(data),
'data': data
}
@app.route('/_api/v1/dopost', methods=['POST'])
@ -191,24 +179,32 @@ def search():
def do_post():
u = require_token()
allow_search = request.form.get('allow_search')
print(allow_search)
content = request.form.get('text')
content = content.strip() if content else None
content = content.strip() if content else None
content = '[tmp]\n' + content if u.name[:4] == 'tmp_' else content
post_type = request.form.get('type')
cw = request.form.get('cw')
cw = cw.strip() if cw else None
cw = cw.strip() if cw else None
if not content or len(content) > 4096:
abort(422)
if cw and len(cw) > 32:
abort(422)
if not content or len(content) > 4096: abort(422)
if cw and len(cw)>32: abort(422)
search_text = content.replace(
'\n', '') if allow_search else ''
p = Post(
name_hash = hash_name(u.name),
content = content,
post_type = post_type,
cw = cw or None,
likenum = 1,
comments = []
)
name_hash=hash_name(u.name),
content=content,
search_text=search_text,
post_type=post_type,
cw=cw or None,
likenum=1,
comments=[]
)
if post_type == 'text':
pass
@ -221,20 +217,21 @@ def do_post():
db.session.add(p)
db.session.commit()
tags = re.findall('(^|\s)#([^#\s]{1,32})', content)
#print(tags)
tags = re.findall('(^|\\s)#([^#\\s]{1,32})', content)
# print(tags)
for t in tags:
tag = t[1]
if not re.match('\d+', tag):
if not re.match('\\d+', tag):
db.session.add(TagRecord(tag=tag, pid=p.id))
db.session.add(Attention(name_hash=hash_name(u.name), pid=p.id))
db.session.commit()
return {
'code': 0,
'date': p.id
}
'code': 0,
'date': p.id
}
@app.route('/_api/v1/editcw', methods=['POST'])
@limiter.limit("50 / hour; 1 / 2 second")
@ -244,17 +241,21 @@ def edit_cw():
cw = request.form.get('cw')
pid = get_num(request.form.get('pid'))
cw = cw.strip() if cw else None
if cw and len(cw)>32: abort(422)
cw = cw.strip() if cw else None
if cw and len(cw) > 32:
abort(422)
post = Post.query.get(pid)
if not post: abort(404)
if post.deleted: abort(451)
if not post:
abort(404)
if post.deleted:
abort(451)
if not (u.name in app.config.get('ADMINS') or hash_name(u.name) == post.name_hash):
if not (u.name in app.config.get('ADMINS')
or hash_name(u.name) == post.name_hash):
abort(403)
post.cw = cw;
post.cw = cw
db.session.commit()
return {'code': 0}
@ -267,17 +268,20 @@ def get_comment():
pid = get_num(request.args.get('pid'))
post = Post.query.get(pid)
if not post: abort(404)
if post.deleted: abort(451)
if not post:
abort(404)
if post.deleted:
abort(451)
data = map_comment(post, u.name)
return {
'code': 0,
'attention': check_attention(u.name, pid),
'likenum': post.likenum,
'data': data
}
'code': 0,
'attention': check_attention(u.name, pid),
'likenum': post.likenum,
'data': data
}
@app.route('/_api/v1/docomment', methods=['POST'])
@limiter.limit("50 / hour; 1 / 3 second")
@ -287,42 +291,51 @@ def do_comment():
pid = get_num(request.form.get('pid'))
post = Post.query.get(pid)
if not post: abort(404)
if post.deleted: abort(451)
if not post:
abort(404)
if post.deleted:
abort(451)
content = request.form.get('text')
content = content.strip() if content else None
content = content.strip() if content else None
content = '[tmp]\n' + content if u.name[:4] == 'tmp_' else content
if not content or len(content) > 4096: abort(422)
if not content or len(content) > 4096:
abort(422)
c = Comment(
name_hash = hash_name(u.name),
content = content,
)
name_hash=hash_name(u.name),
content=content,
)
post.comments.append(c)
post.comment_timestamp = c.timestamp
db.session.commit()
return {
'code': 0,
'data': pid
}
'code': 0,
'data': pid
}
@app.route('/_api/v1/attention', methods=['POST'])
@limiter.limit("200 / hour; 1 / second")
def attention():
u = require_token()
if u.name[:4] == 'tmp_': abort(403)
if u.name[:4] == 'tmp_':
abort(403)
s = request.form.get('switch')
if s not in ['0', '1']: abort(422)
if s not in ['0', '1']:
abort(422)
pid = get_num(request.form.get('pid'))
post = Post.query.get(pid)
if not post: abort(404)
if not post:
abort(404)
at = Attention.query.filter_by(name_hash=hash_name(u.name), pid=pid).first()
at = Attention.query.filter_by(
name_hash=hash_name(
u.name), pid=pid).first()
if not at:
at = Attention(name_hash=hash_name(u.name), pid=pid, disabled=True)
@ -330,32 +343,36 @@ def attention():
if(at.disabled != (s == '0')):
at.disabled = (s == '0')
post.likenum += 1 - 2 * int(s == '0');
post.likenum += 1 - 2 * int(s == '0')
db.session.commit()
return {
'code': 0,
'likenum': post.likenum,
'attention': (s=='1')
}
'code': 0,
'likenum': post.likenum,
'attention': (s == '1')
}
@app.route('/_api/v1/getattention')
def get_attention():
u = require_token()
ats = Attention.query.filter_by(name_hash=hash_name(u.name), disabled=False)
ats = Attention.query.filter_by(
name_hash=hash_name(
u.name), disabled=False)
posts = [Post.query.get(at.pid) for at in ats.all()]
data = [ map_post(post, u.name, 10)
data = [map_post(post, u.name, 10)
for post in posts[::-1]
if post and not post.deleted
]
if post and not post.deleted
]
return {
'code': 0,
'count': len(data),
'data': data
}
'code': 0,
'count': len(data),
'data': data
}
@app.route('/_api/v1/delete', methods=['POST'])
@limiter.limit("50 / hour; 1 / 3 second")
@ -366,18 +383,21 @@ def delete():
obj_id = get_num(request.form.get('id'))
note = request.form.get('note')
if note and len(note)>100: abort(422)
if note and len(note) > 100:
abort(422)
obj = None
if obj_type == 'pid':
obj = Post.query.get(obj_id)
elif obj_type == 'cid':
obj = Comment.query.get(obj_id)
if not obj: abort(404)
if not obj:
abort(404)
if obj.name_hash == hash_name(u.name):
if obj_type == 'pid':
if len(obj.comments): abort(403)
if len(obj.comments):
abort(403)
Attention.query.filter_by(pid=obj.id).delete()
TagRecord.query.filter_by(pid=obj.id).delete()
db.session.delete(obj)
@ -389,31 +409,33 @@ def delete():
log_type='ADMIN DELETE',
log_detail=f"{obj_type}={obj_id}\n{note}",
name_hash=hash_name(u.name)
))
))
if note.startswith('!ban'):
db.session.add(Syslog(
log_type='BANNED',
log_detail=f"=> {obj_type}={obj_id}",
name_hash=obj.name_hash
))
))
else:
abort(403)
db.session.commit()
return {'code': 0}
@app.route('/_api/v1/systemlog')
def system_log():
u = require_token()
require_token()
ss = Syslog.query.order_by(db.desc('timestamp')).limit(100).all()
return {
'start_time': app.config['START_TIME'],
'salt': look(app.config['SALT']),
'tmp_token': tmp_token(),
'data' : list(map(map_syslog, ss))
}
'start_time': app.config['START_TIME'],
'salt': look(app.config['SALT']),
'tmp_token': tmp_token(),
'data': list(map(map_syslog, ss))
}
@app.route('/_api/v1/report', methods=['POST'])
@limiter.limit("50 / hour; 1 / 3 second")
@ -425,10 +447,10 @@ def report():
reason = request.form.get('reason', '')
db.session.add(Syslog(
log_type='REPORT',
log_detail=f"pid={pid}\n{reason}",
name_hash=hash_name(u.name)
))
log_type='REPORT',
log_detail=f"pid={pid}\n{reason}",
name_hash=hash_name(u.name)
))
db.session.commit()
return {'code': 0}
@ -436,4 +458,3 @@ def report():
if __name__ == '__main__':
app.run(debug=True)

9
models.py

@ -3,6 +3,7 @@ import time
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(16))
@ -11,10 +12,12 @@ class User(db.Model):
def __repr__(self):
return f"{self.name}({self.token})"
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
name_hash = db.Column(db.String(64))
content = db.Column(db.String(4096))
search_text = db.Column(db.String(4096), default='', index=True)
post_type = db.Column(db.String(8))
cw = db.Column(db.String(32))
file_url = db.Column(db.String(256))
@ -32,6 +35,7 @@ class Post(db.Model):
def __repr__(self):
return f"{self.name_hash}:[{self.content}]"
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
name_hash = db.Column(db.String(64))
@ -40,7 +44,7 @@ class Comment(db.Model):
deleted = db.Column(db.Boolean, default=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'),
nullable=False)
nullable=False)
def __init__(self, **kwargs):
super(Comment, self).__init__(**kwargs)
@ -49,17 +53,20 @@ class Comment(db.Model):
def __repr__(self):
return f"{self.name_hash}:[{self.content}->{self.post_id}]"
class Attention(db.Model):
id = db.Column(db.Integer, primary_key=True)
name_hash = db.Column(db.String(64))
pid = db.Column(db.Integer)
disabled = db.Column(db.Boolean, default=False)
class TagRecord(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.String(32))
pid = db.Column(db.Integer)
class Syslog(db.Model):
id = db.Column(db.Integer, primary_key=True)
log_type = db.Column(db.String(16))

14
requirements.txt

@ -1,7 +1,7 @@
Flask==1.1.2
Flask-Limit==1.0.2
Flask-Limiter==1.3.1
Flask-Login==0.5.0
Flask-Migrate==2.5.3
Flask-SQLAlchemy==2.4.4
Mastodon.py==1.5.1
Flask>=1.1.2
Flask-Limit>=1.0.2
Flask-Limiter>=1.3.1
Flask-Login>=0.5.0
Flask-Migrate>=2.5.3
Flask-SQLAlchemy>=2.4.4
Mastodon.py>=1.5.1

81
utils.py

@ -1,44 +1,57 @@
import hashlib, time
import hashlib
import time
from flask import request, abort, current_app
from models import User, Attention, Syslog
def get_config(key):
return current_app.config.get(key)
def tmp_token():
return hash_name(str(int(time.time() / 900)) + User.query.get(1).token)[5:21]
return hash_name(str(int(time.time() / 900)) +
User.query.get(1).token)[5:21]
def require_token():
token = request.headers.get('User-Token') or request.args.get('user_token')
if not token: abort(401)
if not token:
abort(401)
if len(token.split('_')) == 2 and get_config('ENABLE_TMP'):
tt, suf = token.split('_')
if tt != tmp_token(): abort(401)
return User(name='tmp_'+suf)
if tt != tmp_token():
abort(401)
return User(name='tmp_' + suf)
u = User.query.filter_by(token=token).first()
if not u or Syslog.query.filter_by(log_type='BANNED', name_hash=hash_name(u.name)).first(): abort(401)
if not u or Syslog.query.filter_by(
log_type='BANNED', name_hash=hash_name(u.name)).first():
abort(401)
return u
def hash_name(name):
return hashlib.sha256((get_config('SALT') + name).encode('utf-8')).hexdigest()
return hashlib.sha256(
(get_config('SALT') + name).encode('utf-8')).hexdigest()
def map_post(p, name, mc=50):
return {
'pid': p.id,
'likenum': p.likenum,
'cw': p.cw,
'text': p.content,
'timestamp': p.timestamp,
'type' : p.post_type,
'url' : p.file_url,
'reply': len(p.comments),
'comments': map_comment(p, name) if len(p.comments) < mc else None,
'attention': check_attention(name, p.id),
'can_del': check_can_del(name, p.name_hash)
}
'pid': p.id,
'likenum': p.likenum,
'cw': p.cw,
'text': p.content,
'timestamp': p.timestamp,
'type': p.post_type,
'url': p.file_url,
'reply': len(p.comments),
'comments': map_comment(p, name) if len(p.comments) < mc else None,
'attention': check_attention(name, p.id),
'can_del': check_can_del(name, p.name_hash),
'allow_search': bool(p.search_text)
}
def map_comment(p, name):
@ -56,27 +69,37 @@ def map_comment(p, name):
'text': c.content,
'timestamp': c.timestamp,
'can_del': check_can_del(name, c.name_hash)
} for c in p.comments if not (c.deleted and gen_name_id(c.name_hash)>=0)
} for c in p.comments if not (c.deleted and gen_name_id(c.name_hash) >= 0)
]
def map_syslog(s):
return {
'type': s.log_type,
'detail': s.log_detail,
'user': look(s.name_hash),
'timestamp': s.timestamp
}
'type': s.log_type,
'detail': s.log_detail,
'user': look(s.name_hash),
'timestamp': s.timestamp
}
def check_attention(name, pid):
at = Attention.query.filter_by(name_hash=hash_name(name), pid=pid, disabled=False).first()
at = Attention.query.filter_by(
name_hash=hash_name(name),
pid=pid,
disabled=False).first()
return 1 if at else 0
def check_can_del(name, author_hash):
return 1 if hash_name(name) == author_hash or name in get_config('ADMINS') else 0
return 1 if hash_name(
name) == author_hash or name in get_config('ADMINS') else 0
def look(s):
return s[:3] + '...' + s[-3:]
def get_num(p):
if not (p and p.isdigit()): abort(422)
if not (p and p.isdigit()):
abort(422)
return int(p)

Loading…
Cancel
Save