2021的特普通奖
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

336 lines
9.5 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. from functools import wraps
  4. from flask import (Flask, request, render_template, send_from_directory, abort,
  5. redirect, session, Blueprint, url_for)
  6. from flask_sqlalchemy import SQLAlchemy
  7. from flask_limiter import Limiter
  8. from flask_limiter.util import get_remote_address
  9. from flask_caching import Cache
  10. from mastodon import Mastodon
  11. import random
  12. from config import C
  13. WRONG_ANS_HTML = '''<html>
  14. <head>
  15. <meta charset='UTF-8'>
  16. <meta name='viewport' content='width=device-width initial-scale=1'>
  17. <title></title>
  18. </head>
  19. <body>
  20. <h1></h1>
  21. <a href="##" onclick="window.history.back()">退</a>
  22. </body>
  23. </html>
  24. '''
  25. MIN_LIKE_NUM = 5
  26. app = Flask(__name__)
  27. app.config.from_object('config.C')
  28. app.secret_key = C.session_key
  29. cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
  30. cache.init_app(app)
  31. MAST_LOGIN_URL = Mastodon(api_base_url=C.mast_base_uri).auth_request_url(
  32. client_id = C.mast_client_id,
  33. redirect_uris = C.mast_redirect_uri, scopes = ['read:accounts']
  34. )
  35. limiter = Limiter(
  36. app,
  37. key_func=get_remote_address,
  38. default_limits=["50 / minute"],
  39. )
  40. db = SQLAlchemy(app)
  41. class Story(db.Model):
  42. id = db.Column(db.Integer, primary_key=True)
  43. title = db.Column(db.String(30))
  44. avatar = db.Column(db.String(128))
  45. text = db.Column(db.Text)
  46. tail = db.Column(db.Integer) # 最后一个 Paragraph 的 id
  47. total_like_num = db.Column(db.Integer, default=0)
  48. is_tree = db.Column(db.Boolean, default=False)
  49. def text_abstract(self, limit=200):
  50. return self.text[:limit] + ("..." if len(self.text) > limit else "")
  51. class Paragraph(db.Model):
  52. id = db.Column(db.Integer, primary_key=True)
  53. parent_id = db.Column(db.Integer, default=0, index=True)
  54. story_id = db.Column(db.Integer, index=True)
  55. is_hidden = db.Column(db.Boolean, default=False)
  56. is_chosen = db.Column(db.Boolean, default=False)
  57. text = db.Column(db.Text)
  58. author = db.Column(db.String(30))
  59. time = db.Column(db.DateTime, default=datetime.now)
  60. like_num = db.Column(db.Integer, default=0, index=True)
  61. angry_num = db.Column(db.Integer, default=0)
  62. fun_num = db.Column(db.Integer, default=0)
  63. sweat_num = db.Column(db.Integer, default=0)
  64. @property
  65. def create_at(self):
  66. return self.time.strftime("%m-%d %H:%M")
  67. def reaction_status(self):
  68. user = session['uid']
  69. return list(zip(
  70. '👍😡🤣😅👎',
  71. [self.like_num, self.angry_num, self.fun_num, self.sweat_num, 0],
  72. [
  73. user and bool(Reaction.query.filter_by(pid=self.id, user=user, kind=i).first())
  74. for i in range(1, 6)
  75. ],
  76. range(1, 6)
  77. ))
  78. class Reaction(db.Model):
  79. id = db.Column(db.Integer, primary_key=True)
  80. kind = db.Column(db.SmallInteger) # 1: like 2: angry 3: funny 4: sweat 5:dislike
  81. pid = db.Column(db.Integer, index=True) # id of paragraph
  82. user = db.Column(db.String(30)) # str(uid)
  83. def choose_new_next(min_like_num=MIN_LIKE_NUM):
  84. for story in Story.query.filter_by(is_tree=False).all():
  85. last_paragraph_id = story.tail
  86. next_one = Paragraph.query.filter_by(story_id=story.id, parent_id=last_paragraph_id, is_hidden=False)\
  87. .order_by(Paragraph.like_num.desc()).first()
  88. if next_one and next_one.like_num >= min_like_num:
  89. story.text += next_one.text
  90. story.total_like_num += next_one.like_num
  91. story.tail = next_one.id
  92. next_one.is_chosen = True
  93. for story in Story.query.filter_by(is_tree=True).all():
  94. _max = story.total_like_num
  95. _tail = story.tail
  96. _sum_dict = {}
  97. def _get_like_sum(p):
  98. if p.id in _sum_dict:
  99. return _sum_dict[p.id]
  100. s = p.like_num + (p.parent_id and _get_like_sum(Paragraph.query.get(p.parent_id)))
  101. _sum_dict[p.id] = s
  102. return s
  103. for p in Paragraph.query.filter(Paragraph.like_num >= min_like_num)\
  104. .filter_by(story_id=story.id, is_hidden=False).all():
  105. if _get_like_sum(p) > _max:
  106. _max = _get_like_sum(p)
  107. _tail = p.id
  108. p = Paragraph.query.get(_tail)
  109. _text = p.text
  110. while p.parent_id:
  111. p = Paragraph.query.get(p.parent_id)
  112. _text = p.text + _text
  113. story.total_like_num = _max
  114. story.tail = _tail
  115. story.text = _text
  116. db.session.commit()
  117. def sample_question(qs, n=3):
  118. random.seed(session['uid'])
  119. return random.sample(qs, n)
  120. def login_required(func):
  121. @wraps(func)
  122. def warp(*args, **kwargs):
  123. if not session.get('username'):
  124. abort(403)
  125. return func(*args, **kwargs)
  126. return warp
  127. bp = Blueprint('main_bp', __name__, url_prefix='/ordinary')
  128. @bp.before_request
  129. def set_uid():
  130. if 'uid' not in session:
  131. session['uid'] = random.randint(0, 10000000)
  132. @bp.route('/static/<path:path>')
  133. def send_static(path):
  134. return send_from_directory('static/', path)
  135. @bp.route('/guest_login', methods=['POST'])
  136. @limiter.limit("5 / hour")
  137. def guest_login():
  138. for name, ques, hint, ans in sample_question(C.verify_questions):
  139. if request.form.get(name) != ans:
  140. return WRONG_ANS_HTML, 401
  141. session['username'] = 'guest<%s>' % session['uid']
  142. session.pop('avatar', None)
  143. session.permanent = True
  144. return redirect(request.referrer)
  145. @bp.route('/mast_auth')
  146. def mast_login_auth():
  147. code = request.args.get('code')
  148. client = Mastodon(
  149. client_id=C.mast_client_id,
  150. client_secret=C.mast_client_sec,
  151. api_base_url=C.mast_base_uri
  152. )
  153. client.log_in(code=code, redirect_uri=C.mast_redirect_uri,
  154. scopes=['read:accounts'])
  155. info = client.account_verify_credentials()
  156. session['username'] = info.acct
  157. session['avatar'] = info.avatar
  158. session.permanent = True
  159. return redirect('.')
  160. @bp.route('/logout')
  161. def logout():
  162. session.pop('username', None)
  163. session.pop('avatar', None)
  164. return redirect('.')
  165. @bp.route('/')
  166. @cache.cached(timeout=10)
  167. def home():
  168. stories = Story.query.order_by(Story.total_like_num.desc()).all()
  169. username = session.get('username', '')
  170. avatar = session.get('avatar', C.guest_avatar)
  171. cs_login_url = MAST_LOGIN_URL
  172. guest_login_url = url_for('main_bp.guest_login')
  173. verify_questions = sample_question(C.verify_questions)
  174. return render_template('home.html', **locals())
  175. @bp.route('/<int:story_id>')
  176. def story(story_id):
  177. story = Story.query.get_or_404(story_id)
  178. is_tree = story.is_tree
  179. draft = ''
  180. if is_tree:
  181. tail = request.args.get('tail', story.tail, type=int)
  182. p_tail = Paragraph.query.get_or_404(tail)
  183. if p_tail.story_id != story_id:
  184. abort(404)
  185. paragraph_part = [p_tail]
  186. p = p_tail
  187. while p.parent_id:
  188. p = Paragraph.query.get_or_404(p.parent_id)
  189. paragraph_part.insert(0, p)
  190. else:
  191. tail = story.tail
  192. paragraph_part = Paragraph.query.filter_by(
  193. story_id=story_id, is_chosen=True, is_hidden=False
  194. ).all()
  195. if 'username' in session:
  196. last_post = Paragraph.query.filter_by(
  197. story_id=story_id, author=session['username'], is_chosen=False
  198. ).order_by(Paragraph.id.desc()).first()
  199. if last_post and last_post.parent_id != tail:
  200. draft = last_post.text
  201. sort_by = request.args.get('sort_by', 'time')
  202. q = Paragraph.query.filter_by(story_id=story_id, parent_id=tail, is_hidden=False)
  203. q = q.order_by(Paragraph.like_num.desc() if sort_by == 'like' else
  204. Paragraph.id.desc())
  205. pagination = q.paginate(max_per_page=100)
  206. username = session.get('username', '')
  207. avatar = session.get('avatar', C.guest_avatar)
  208. cs_login_url = MAST_LOGIN_URL
  209. guest_login_url = url_for('main_bp.guest_login')
  210. verify_questions = sample_question(C.verify_questions)
  211. min_like_num = MIN_LIKE_NUM
  212. email = C.email
  213. return render_template('story.html', **locals())
  214. @bp.route('/create', methods=['POST'])
  215. @login_required
  216. @limiter.limit("66 / hour")
  217. def create():
  218. story_id = request.form.get('story-id')
  219. text = request.form.get('text')
  220. if not text or len(text) > 140:
  221. abort(422)
  222. story = Story.query.get_or_404(story_id)
  223. if story.is_tree:
  224. parent_id = request.form.get('tail', type=int)
  225. else:
  226. parent_id = story.tail
  227. p = Paragraph(
  228. parent_id=parent_id,
  229. story_id=story_id,
  230. text=text,
  231. author=session['username']
  232. )
  233. db.session.add(p)
  234. db.session.commit()
  235. return redirect(story_id)
  236. @bp.route('/react', methods=['POST'])
  237. @limiter.limit("100 / minute")
  238. def react():
  239. kind = request.form.get('kind', type=int)
  240. pid = request.form.get('pid', type=int)
  241. if kind not in range(1, 6):
  242. abort(422)
  243. p = Paragraph.query.get_or_404(pid)
  244. d = dict(kind=kind, user=session['uid'], pid=pid)
  245. if Reaction.query.filter_by(**d).first():
  246. return ''
  247. db.session.add(Reaction(**d))
  248. n = ''
  249. if kind == 1:
  250. p.like_num += 1
  251. n = p.like_num
  252. elif kind == 2:
  253. p.angry_num += 1
  254. n = p.angry_num
  255. elif kind == 3:
  256. p.fun_num += 1
  257. n = p.fun_num
  258. elif kind == 4:
  259. p.sweat_num += 1
  260. n = p.sweat_num
  261. db.session.commit()
  262. return str(n)
  263. app.register_blueprint(bp)
  264. if __name__ == '__main__':
  265. app.run(debug=True)