web_panels.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. from flask import flash, render_template, Blueprint, current_app, redirect, request, url_for, session, g
  2. from datetime import timezone
  3. from ownchatbot.db import get_db, reread_goals, reread_votes, rem_vote, reset_vote, reset_goal, clear_fulfilled_rewards, clear_reward_queue, rem_cool, rem_from_queue
  4. from ownchatbot.reward_handlers import all_active_votes, all_active_goals, all_active_rewards, get_queue, fulfill_reward, save_rewards, activate_category, deactivate_category, refund_reward, reread_categories, save_config
  5. from ownchatbot.user_handlers import get_all_users, get_all_users_by_name, refund_points, adjust_points, change_email, get_email_code, del_email_code
  6. from ownchatbot.bot_messages import save_announce
  7. from ownchatbot.owncast_com import send_private_chat
  8. import json
  9. import emoji
  10. from ownchatbot.kofi_handlers import save_kofi_settings, kofi_pngs
  11. import random
  12. import pkce
  13. import requests
  14. from functools import wraps
  15. ocb = Blueprint('web_panels', __name__)
  16. state_value = ''
  17. def requires_login(f):
  18. @wraps(f)
  19. def decorated_function(*args, **kwargs):
  20. if 'user' not in session:
  21. return redirect(url_for('web_panels.login'))
  22. return f(*args, **kwargs)
  23. return decorated_function
  24. @ocb.route('/login')
  25. def login(): # Verify the streamer using indieauth, to their owncast instance
  26. code_verifier, code_challenge = pkce.generate_pkce_pair() # Generate a code verifier and code challenge
  27. global state_value
  28. state_value = code_verifier
  29. owncast_url = current_app.config['OWNCAST_URL']
  30. client_id = current_app.config['INDIEAUTH_CLIENT_ID']
  31. redirect_url = f'{owncast_url}/api/auth/provider/indieauth?client_id={client_id}&redirect_uri={url_for("web_panels.auth_response", _external=True)}&response_type=code&code_challenge_method=S256&code_challenge={code_challenge}&state={code_verifier}'
  32. return redirect(redirect_url)
  33. @ocb.route('/auth_response')
  34. def auth_response():
  35. code = request.args.get('code')
  36. state = request.args.get('state')
  37. if state == state_value: # Check that the state value returned matches the state value sent
  38. current_app.logger.info(f'Valid CSRF Code. Streamer authenticated.')
  39. user_info = 'code'
  40. session['user'] = user_info
  41. return redirect(url_for('web_panels.mgmt'))
  42. else:
  43. current_app.logger.info(f'Invalid CSRF Code. Streamer not authenticated.')
  44. return 'Not Authorized'
  45. @ocb.route('/logout')
  46. def logout():
  47. session.pop('user', None)
  48. return redirect(url_for('web_panels.user_panel'))
  49. @ocb.route('/mgmt', methods=['GET']) # The streamer's management panel
  50. @requires_login
  51. def mgmt():
  52. owncast_url = current_app.config['OWNCAST_URL']
  53. db = get_db()
  54. users = get_all_users(db)
  55. utc_timezone = timezone.utc
  56. rewards = current_app.config['REWARDS']
  57. active_rewards = []
  58. for each_reward in all_active_rewards(): # Get the name of all active rewards
  59. active_rewards.append(each_reward)
  60. active_categories = current_app.config['ACTIVE_CAT']
  61. inactive_categories = current_app.config['INACTIVE_CAT']
  62. all_cats = current_app.config['ALL_CAT']
  63. mgmt_auth = current_app.config['MGMT_AUTH']
  64. points_interval = current_app.config['POINTS_INTERVAL']
  65. points_award = current_app.config['POINTS_AWARD']
  66. gunicorn_logging = current_app.config['GUNICORN']
  67. prefix = current_app.config['PREFIX']
  68. access_token = current_app.config['ACCESS_TOKEN']
  69. kofi_token = current_app.config['KOFI_TOKEN']
  70. kofi_integration = current_app.config['KOFI_INTEGRATION']
  71. kofi_logos = kofi_pngs()
  72. announce_enable = current_app.config['ANNOUNCE_ENABLE']
  73. announce_interval = current_app.config['ANNOUNCE_INTERVAL']
  74. announcements = current_app.config['ANNOUNCEMENTS']
  75. settings_info = [
  76. mgmt_auth,
  77. points_interval,
  78. points_award,
  79. gunicorn_logging,
  80. prefix,
  81. access_token,
  82. owncast_url,
  83. kofi_token,
  84. kofi_integration,
  85. announce_enable,
  86. announce_interval
  87. ]
  88. return render_template('mgmt.html',
  89. queue=get_queue(db),
  90. votes=all_active_votes(db),
  91. goals=all_active_goals(db),
  92. rewards=rewards,
  93. active_rewards=active_rewards,
  94. prefix=current_app.config['PREFIX'],
  95. kofi_settings=current_app.config['KOFI_SETTINGS'],
  96. kofi_integration=kofi_integration,
  97. kofi_logos=kofi_logos,
  98. announcements=announcements,
  99. users=users,
  100. utc_timezone=utc_timezone,
  101. active_categories=active_categories,
  102. inactive_categories=inactive_categories,
  103. settings_info=settings_info)
  104. @ocb.route('/userpanel', methods=['GET']) # The viewers panel
  105. def user_panel():
  106. db = get_db()
  107. instance = request.args.get('instance')
  108. all_rewards = rewards = current_app.config['REWARDS']
  109. username = request.args.get('username')
  110. points_interval = current_app.config['POINTS_INTERVAL']
  111. points_award = current_app.config['POINTS_AWARD']
  112. if username is not None:
  113. users = get_all_users_by_name(db, username)
  114. else:
  115. users = []
  116. utc_timezone = timezone.utc
  117. return render_template('userpanel.html',
  118. queue=get_queue(db),
  119. votes=all_active_votes(db),
  120. goals=all_active_goals(db),
  121. rewards=all_active_rewards(),
  122. all_rewards=all_rewards,
  123. prefix=current_app.config['PREFIX'],
  124. kofi_settings=current_app.config['KOFI_SETTINGS'],
  125. kofi_integration=current_app.config['KOFI_INTEGRATION'],
  126. points_interval=points_interval,
  127. points_award=points_award,
  128. username=username,
  129. users=users,
  130. instance=instance,
  131. utc_timezone=utc_timezone)
  132. @ocb.route('/mgmt/fulfill', methods=['GET'])
  133. @requires_login
  134. def fulfilled():
  135. db = get_db()
  136. reward_id = request.args.get('reward_id')
  137. username = request.args.get('username')
  138. fulfill_reward(db, reward_id)
  139. return redirect(url_for('web_panels.mgmt'))
  140. @ocb.route('/mgmt/refund', methods=['GET'])
  141. @requires_login
  142. def refund():
  143. db = get_db()
  144. reward_id = request.args.get('reward_id')
  145. reward = request.args.get('reward')
  146. rewards = current_app.config['REWARDS']
  147. points = rewards[reward]['price']
  148. username = request.args.get('username')
  149. user_id = request.args.get('rewarder_id')
  150. refund_points(db, user_id, points) # resets points
  151. refund_reward(db, reward_id) # marks the reward as refunded
  152. return redirect(url_for('web_panels.mgmt'))
  153. @ocb.route('/mgmt/edit_account/<user_id>', methods=['GET', 'POST']) # Streamer manually edit user's account
  154. @requires_login
  155. def edit_account(user_id):
  156. db = get_db()
  157. name = request.args.get('name')
  158. points = request.args.get('points')
  159. email = request.args.get('email')
  160. if request.method == 'POST':
  161. user_id = request.form['user_id']
  162. name = request.form['name']
  163. newpoints = request.form['newpoints']
  164. adjust_points(db, user_id, newpoints)
  165. newemail = request.form['newemail']
  166. if newemail == 'None':
  167. current_app.logger.info(f'No email change requested')
  168. else:
  169. if change_email(db, user_id, newemail):
  170. if newemail == '':
  171. current_app.logger.info(f'Removed {name}\'s email')
  172. else:
  173. current_app.logger.info(f'Changed {name}\'s email to {newemail}')
  174. return redirect(url_for('web_panels.mgmt'))
  175. return render_template('edit_account.html',
  176. name=name,
  177. user_id=user_id,
  178. points=points,
  179. email=email)
  180. @ocb.route('/mgmt/delete/<reward_name>', methods=['GET', 'POST'])
  181. @requires_login
  182. def delete(reward_name):
  183. del_reward = current_app.config['REWARDS']
  184. del_reward.pop(reward_name)
  185. if save_rewards(del_reward):
  186. if rem_cool(reward_name):
  187. rem_from_queue(reward_name)
  188. if reread_votes():
  189. if reread_goals():
  190. pass
  191. return redirect(url_for('web_panels.mgmt'))
  192. @ocb.route('/mgmt/edit/<reward_name>', methods=['GET', 'POST'])
  193. @requires_login
  194. def edit(reward_name):
  195. active_categories = current_app.config['ACTIVE_CAT']
  196. all_the_rewards = current_app.config['REWARDS']
  197. reward_data = all_the_rewards[reward_name]
  198. all_cats = current_app.config['ALL_CAT']
  199. if request.method == 'POST':
  200. reward_data['cooldown'] = int(request.form['cooldown'])
  201. reward_data['type'] = request.form['type']
  202. if reward_data['type'] == 'goal':
  203. reward_data['target'] = int(request.form['target'])
  204. if "milestones" not in reward_data: # If using old rewards.py, and no milestones key exists, create one
  205. reward_data["milestones"] = {"milestone1": [], "milestone2": [], "milestone3": []}
  206. if request.form['milestone1_points'] == '':
  207. reward_data['milestones']['milestone1'] = []
  208. else:
  209. milestone1_points = int(request.form['milestone1_points'])
  210. reward_data['milestones']['milestone1'] = [request.form['milestone1_desc'], milestone1_points]
  211. if request.form['milestone2_points'] == '':
  212. reward_data['milestones']['milestone2'] = []
  213. else:
  214. milestone2_points = int(request.form['milestone2_points'])
  215. reward_data['milestones']['milestone2'] = [request.form['milestone2_desc'], milestone2_points]
  216. if request.form['milestone3_points'] == '':
  217. reward_data['milestones']['milestone3'] = []
  218. else:
  219. milestone3_points = int(request.form['milestone3_points'])
  220. reward_data['milestones']['milestone3'] = [request.form['milestone3_desc'], milestone3_points]
  221. else:
  222. reward_data['price'] = int(request.form['price'])
  223. reward_data['info'] = emoji.demojize(request.form['info'])
  224. if reward_data['type'] == 'special':
  225. reward_data['cmd'] = request.form['cmd']
  226. reward_data['categories'] = request.form.getlist('category')
  227. reward_data['cooldown'] = int(request.form['cooldown'])
  228. all_the_rewards[reward_name] = reward_data
  229. save_rewards(all_the_rewards)
  230. if reward_data['type'] == 'goal': # Sync goals and votes in the db with rewards.py
  231. reread_goals()
  232. if reward_data['type'] == 'vote':
  233. reread_votes()
  234. return redirect(url_for('web_panels.mgmt'))
  235. return render_template('edit.html',
  236. all_cats=all_cats,
  237. reward_name=reward_name,
  238. active_categories=active_categories,
  239. reward_data=reward_data)
  240. @ocb.route('/mgmt/settings', methods=['GET', 'POST']) # OwnchatBot settings panel
  241. @requires_login
  242. def settings():
  243. points_interval = int(request.form['points_interval'])
  244. points_award = int(request.form['points_award'])
  245. gunicorn_logging = 'gunicorn_logging' in request.form
  246. prefix = request.form['prefix']
  247. access_token = request.form['access_token']
  248. owncast_url = request.form['owncast_url']
  249. mgmt_auth = request.form['mgmt_auth']
  250. kofi_integration = 'kofi_integration' in request.form
  251. kofi_token = request.form['kofi_token']
  252. config_dict = {
  253. 'MGMT_AUTH': mgmt_auth,
  254. 'POINTS_INTERVAL': points_interval,
  255. 'POINTS_AWARD': points_award,
  256. 'GUNICORN': gunicorn_logging,
  257. 'PREFIX': prefix,
  258. 'ACCESS_TOKEN': access_token,
  259. 'OWNCAST_URL': owncast_url,
  260. 'KOFI_TOKEN': kofi_token,
  261. 'KOFI_INTEGRATION': kofi_integration
  262. }
  263. if save_config(config_dict): # Save new config.py
  264. current_app.logger.info('Saved new config.')
  265. return redirect(url_for('web_panels.mgmt'))
  266. @ocb.route('/mgmt/announcements', methods=['GET', 'POST']) # OwnchatBot settings panel
  267. @requires_login
  268. def announcements():
  269. announce_enable = 'announce_enable' in request.form
  270. announce_interval = int(request.form['announce_interval'])
  271. new_announcements = []
  272. new_announcements = request.form['announcements'].strip().split('\n')
  273. announce_dict = {
  274. 'ANNOUNCEMENTS': new_announcements,
  275. 'ANNOUNCE_ENABLE': announce_enable,
  276. 'ANNOUNCE_INTERVAL': announce_interval
  277. }
  278. if save_announce(announce_dict): # Save new announce.py
  279. current_app.logger.info('Saved new announcements.')
  280. return redirect(url_for('web_panels.mgmt'))
  281. @ocb.route('/mgmt/ksettings', methods=['GET', 'POST']) # OwnchatBot settings panel
  282. @requires_login
  283. def ksettings():
  284. kofi_settings_dict = current_app.config['KOFI_SETTINGS']
  285. if request.method == 'POST':
  286. enable_donations = 'enable_donations' in request.form
  287. set_donation_points = request.form['set_donation_points']
  288. enable_subs = 'enable_subs' in request.form
  289. sub_points = int(request.form['sub_points'])
  290. kofi_url = request.form['kofi_url']
  291. kofi_logo = request.form.get('kofi_logo')
  292. kofi_settings_dict['donations'] = enable_donations
  293. kofi_settings_dict['subs'] = enable_subs
  294. kofi_settings_dict['sub_points'] = sub_points
  295. kofi_settings_dict['kofi_url'] = kofi_url
  296. kofi_settings_dict['kofi_logo'] = kofi_logo
  297. if save_kofi_settings(kofi_settings_dict):
  298. current_app.logger.info(f'Saved Kofi settings')
  299. return redirect(url_for('web_panels.mgmt'))
  300. @ocb.route('/mgmt/add/<reward_type>', methods=['GET', 'POST'])
  301. @requires_login
  302. def add(reward_type):
  303. all_cats = current_app.config['ALL_CAT']
  304. active_categories = current_app.config['ACTIVE_CAT']
  305. all_the_rewards = current_app.config['REWARDS']
  306. if request.method == 'POST':
  307. name = request.form['name']
  308. name = name.lower() # Force the name to all lower case
  309. name = emoji.demojize(name) # Remove any emojis
  310. name = name.replace(" ", "") # Remove any spaces from the name
  311. type = request.form['type']
  312. if name in all_the_rewards: # Check for duplicate reward names
  313. flash("A reward with this name already exists.", "error") # Flash an error message
  314. return redirect(url_for('web_panels.add', reward_type=reward_type)) # Redirect back to the add page
  315. if type != 'category': # If we're only adding a category, skip all of this
  316. cooldown = int(request.form['cooldown'])
  317. if type == 'redeem' or type == 'special' or type == 'vote':
  318. price = int(request.form['price'])
  319. if type == 'goal':
  320. target = int(request.form['target'])
  321. milestone1_desc = request.form['milestone1_desc']
  322. if request.form['milestone1_points'] == '':
  323. milestone1_points = ''
  324. else:
  325. milestone1_points = int(request.form['milestone1_points'])
  326. milestone2_desc = request.form['milestone2_desc']
  327. if request.form['milestone2_points'] == '':
  328. milestone2_points = ''
  329. else:
  330. milestone2_points = int(request.form['milestone2_points'])
  331. milestone3_desc = request.form['milestone3_desc']
  332. if request.form['milestone3_points'] == '':
  333. milestone3_points = ''
  334. else:
  335. milestone3_points = int(request.form['milestone3_points'])
  336. info = request.form['info']
  337. info = emoji.demojize(info) # Remove any emojis
  338. if type == 'special':
  339. cmd = request.form['cmd']
  340. categories = request.form.getlist('category')
  341. if type == 'redeem':
  342. if categories == ['']:
  343. all_the_rewards[name] = {'price': price, 'type': type, 'info': info, 'cooldown': cooldown}
  344. else:
  345. all_the_rewards[name] = {'price': price, 'type': type, 'info': info, 'categories': categories, 'cooldown': cooldown}
  346. if type == 'goal':
  347. if categories == ['']:
  348. all_the_rewards[name] = {'target': target, 'type': type, 'info': info, 'cooldown': cooldown}
  349. else:
  350. all_the_rewards[name] = {'target': target, 'type': type, 'info': info, 'categories': categories, 'cooldown': cooldown}
  351. all_the_rewards[name]["milestones"] = {"milestone1": [], "milestone2": [], "milestone3": []} # Create empty milestones key
  352. if milestone1_points:
  353. all_the_rewards[name]["milestones"]["milestone1"] = [milestone1_desc, milestone1_points]
  354. if milestone2_points:
  355. all_the_rewards[name]["milestones"]["milestone2"] = [milestone2_desc, milestone2_points]
  356. if milestone3_points:
  357. all_the_rewards[name]["milestones"]["milestone3"] = [milestone3_desc, milestone3_points]
  358. if type == 'vote':
  359. if categories == ['']:
  360. all_the_rewards[name] = {'price': price, 'type': type, 'info': info}
  361. else:
  362. all_the_rewards[name] = {'price': price, 'type': type, 'info': info, 'categories': categories, 'cooldown': cooldown}
  363. if type == 'special':
  364. if categories == ['']:
  365. all_the_rewards[name] = {'price': price, 'type': type, 'info': info, 'cmd': cmd, 'cooldown': cooldown}
  366. else:
  367. all_the_rewards[name] = {'price': price, 'type': type, 'info': info, 'cmd': cmd, 'categories': categories, 'cooldown': cooldown}
  368. save_rewards(all_the_rewards)
  369. if type == 'goal': # Remove old goals and votes from the database
  370. reread_goals()
  371. if type == 'vote':
  372. reread_votes()
  373. else: # If we're only adding a category
  374. inactive_categories = current_app.config['INACTIVE_CAT']
  375. inactive_categories.append(name) # Add it to the INACTIVE_CAT variable
  376. reread_categories() # Write it to categories.py
  377. return redirect(url_for('web_panels.mgmt'))
  378. return render_template('add.html',
  379. all_cats=all_cats,
  380. reward_type=reward_type,
  381. active_categories=active_categories)
  382. @ocb.route('/set_viewer_email', methods=['GET', 'POST'])
  383. def set_viewer_email():
  384. db = get_db()
  385. mail_reg_code = int(request.form['code'])
  386. user_id = request.form['user_id']
  387. db_mail_reg_code = get_email_code(db, user_id)
  388. new_email = request.form['new_email']
  389. instance = request.form['instance']
  390. user_name = request.form['user_name']
  391. if mail_reg_code == db_mail_reg_code:
  392. if change_email(db, user_id, new_email):
  393. del_email_code(db, user_id)
  394. flash(f"Email Address \"{new_email}\" successfully registered.", "success")
  395. send_private_chat(user_id, f'{user_name}, thanks for registering for Kofi perks! I appreciate your support!')
  396. current_app.logger.info(f'Changed {user_id}\'s email to {new_email}')
  397. else:
  398. flash(f"Incorrect code. Email Address \"{new_email}\" was not registered.", "failure")
  399. current_app.logger.info(f'The code entered, \"{mail_reg_code}\", does not match \"{db_mail_reg_code}\" found in database.')
  400. return redirect(url_for('web_panels.user_panel', instance=instance, username=user_name))
  401. @ocb.route('/mgmt/activate/<category>', methods=['GET', 'POST'])
  402. def activate(category):
  403. activate_category(category)
  404. return redirect(url_for('web_panels.mgmt'))
  405. @ocb.route('/mgmt/deactivate/<category>', methods=['GET', 'POST'])
  406. def deactivate(category):
  407. deactivate_category(category)
  408. return redirect(url_for('web_panels.mgmt'))
  409. @ocb.route('/mgmt/delcat/<cat_name>/<cat_act>', methods=['GET', 'POST'])
  410. def delcat(cat_name, cat_act):
  411. active_categories = current_app.config['ACTIVE_CAT']
  412. inactive_categories = current_app.config['INACTIVE_CAT']
  413. if cat_act == 'inactive':
  414. inactive_categories.remove(cat_name)
  415. else:
  416. active_categories.remove(cat_name)
  417. reread_categories()
  418. current_rewards = current_app.config['REWARDS']
  419. for reward, details in current_rewards.items(): # Remove from rewards.py as well
  420. if cat_name in details['categories']:
  421. details['categories'].remove(cat_name)
  422. save_rewards(current_rewards)
  423. return redirect(url_for('web_panels.mgmt'))
  424. @ocb.route('/mgmt/reset/<reward_name>/<reward_type>', methods=['GET', 'POST']) # Reset votes and goals to zero
  425. def reset(reward_name, reward_type):
  426. if reward_type == "goal":
  427. reset_goal(reward_name)
  428. if reward_type == "vote":
  429. reset_vote(reward_name)
  430. return redirect(url_for('web_panels.mgmt'))
  431. @ocb.route('/mgmt/rereadvotes', methods=['GET', 'POST'])
  432. def rereadv():
  433. reread_votes()
  434. return redirect(url_for('web_panels.mgmt'))
  435. @ocb.route('/mgmt/clearfulfilled', methods=['GET', 'POST'])
  436. def clearfulfilled():
  437. clear_fulfilled_rewards()
  438. return redirect(url_for('web_panels.mgmt'))
  439. @ocb.route('/mgmt/clearqueue', methods=['GET', 'POST'])
  440. def clear_queue():
  441. clear_reward_queue()
  442. return redirect(url_for('web_panels.mgmt'))