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.

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