reward_handlers.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import os
  2. from flask import current_app
  3. from sqlite3 import Error
  4. from ownchatbot.user_handlers import spend_points
  5. import subprocess
  6. import json
  7. from ownchatbot.owncast_com import send_chat
  8. def sort_key(item): # Sort rewards by price
  9. price = item[1].get('price')
  10. return (price is None, price)
  11. def check_vote(db, vote_name, user_id): # Check if user has already voted on this vote
  12. try:
  13. cursor = db.execute(
  14. "SELECT voters FROM votes WHERE name = ?",
  15. (vote_name,)
  16. )
  17. row = cursor.fetchone()
  18. if row is None:
  19. current_app.logger.error(f'\"{vote_name}\" not found in vote table.')
  20. return False
  21. if row[0] == user_id:
  22. return True
  23. except Error as cverror:
  24. current_app.logger.error(f'Couldn\'t check if {user_id} already voted on \"{vote_name}\": {cverror.args[0]}')
  25. def add_to_vote(db, vote_name, user_id): # Add a count to a vote
  26. try: # Check if vote exists in the database
  27. cursor = db.execute(
  28. "SELECT count FROM votes WHERE name = ?",
  29. (vote_name,)
  30. )
  31. vote = cursor.fetchone()
  32. if vote is None:
  33. current_app.logger.error(f'{vote_name} not found in vote table.')
  34. return False
  35. else: # If vote exists, add a count
  36. db.execute(
  37. "UPDATE votes SET count = count + 1, voters = ? WHERE name = ?",
  38. (user_id, vote_name,)
  39. )
  40. db.commit()
  41. return True
  42. except Error as terror:
  43. current_app.logger.error(f'Couldn\'t add to \"{vote_name}\" vote: {terror.args[0]}')
  44. return False
  45. def add_to_queue(db, user_id, reward_name): # Add a reward to the queue
  46. try:
  47. db.execute(
  48. "INSERT INTO reward_queue(reward, user_id, fulfilled, refunded) VALUES(?, ?, ?, ?)",
  49. (reward_name, user_id, 0, 0)
  50. )
  51. db.commit()
  52. return True
  53. except Error as qerror:
  54. current_app.logger.error(f'Couldn\'t add to reward \"{reward_name}\" for {user_id} queue: {qerror.args[0]}')
  55. return False
  56. def run_script(reward_name, script_cmd): # Run a script form a special reward
  57. try:
  58. subprocess.check_call(script_cmd, shell=True)
  59. except Exception as scerror:
  60. current_app.logger.error(f'Couldn\'t run script \"{reward_name}\": {scerror.args[0]}')
  61. return False
  62. return True
  63. def add_to_goal(db, user_id, reward_name, points_contributed): # Add a contribution to a goal
  64. try:
  65. cursor = db.execute(
  66. "SELECT progress, target FROM goals WHERE name = ?",
  67. (reward_name,)
  68. )
  69. row = cursor.fetchone()
  70. if row is None:
  71. current_app.logger.error(f'\"{reward_name}\" not found in goal table.')
  72. return False
  73. progress, target = row
  74. if progress + points_contributed > target:
  75. points_contributed = target - progress
  76. if points_contributed < 0:
  77. points_contributed = 0
  78. if spend_points(db, user_id, points_contributed):
  79. cursor = db.execute(
  80. "UPDATE goals SET progress = ? WHERE name = ?",
  81. (progress + points_contributed, reward_name)
  82. )
  83. db.commit()
  84. return True
  85. except Error as gerror:
  86. current_app.logger.error(f'Couldn\'t update goal: {gerror.args[0]}')
  87. return False
  88. def goal_left(db, reward_name):
  89. try:
  90. cursor = db.execute(
  91. "SELECT progress, target FROM goals WHERE name = ?",
  92. (reward_name,)
  93. )
  94. row = cursor.fetchone()
  95. if row is None:
  96. current_app.logger.error(f'{reward_name} not found in Goal table.')
  97. else:
  98. progress, target = row
  99. left = target - progress
  100. return left
  101. except Error as glerror:
  102. current_app.logger.error(f'Couldn\'t check progress for \"{reward_name}\" goal: {glerror.args[0]}')
  103. def goal_reached(db, reward_name): # Set a goal as completed
  104. try:
  105. cursor = db.execute(
  106. "SELECT complete FROM goals WHERE name = ?",
  107. (reward_name,)
  108. )
  109. row = cursor.fetchone()
  110. if row is None:
  111. current_app.logger.error(f'{reward_name} not found in goal table.')
  112. else:
  113. return row[0]
  114. except Error as grerror:
  115. current_app.logger.error(f'Couldn\'t check if goal was met: {grerror.args[0]}')
  116. return False
  117. def was_milestone_reached(db, reward_name): # Check if a milestone was reached
  118. try:
  119. all_rewards = current_app.config['REWARDS'] # Get milestone numbers from REWARDS
  120. milestones_info = all_rewards[reward_name]['milestones']
  121. ms1_goal = ms2_goal = ms3_goal = float('inf') # To avoid referencing unassigned variables, in case there aren't any milestones set
  122. ms1_reward = ms2_reward = ms3_reward = None # Initialize to None
  123. if len(milestones_info['milestone1']) > 1 and milestones_info['milestone1'][1]: # Check that there is a value for the milestone
  124. ms1_goal = milestones_info['milestone1'][1]
  125. ms1_reward = f'🚩 \"{milestones_info["milestone1"][0]}\"'
  126. if len(milestones_info['milestone2']) > 1 and milestones_info['milestone2'][1]:
  127. ms2_goal = milestones_info['milestone2'][1]
  128. ms2_reward = f'🚩🚩 \"{milestones_info["milestone2"][0]}\"'
  129. if len(milestones_info['milestone3']) > 1 and milestones_info['milestone3'][1]:
  130. ms3_goal = milestones_info['milestone3'][1]
  131. ms3_reward = f'🚩🚩🚩 \"{milestones_info["milestone3"][0]}\"'
  132. cursor = db.execute(
  133. "SELECT progress, milestones FROM goals WHERE name = ?",
  134. (reward_name,)
  135. ) # Get progress and milestones info from database
  136. row = cursor.fetchone()
  137. if row is None:
  138. current_app.logger.error(f'{reward_name} not found in Goal table.')
  139. else:
  140. progress = int(row['progress'])
  141. milestones = int(row['milestones'])
  142. if progress >= ms1_goal and progress < ms2_goal:
  143. new_milestones = 1
  144. ms_reward = ms1_reward
  145. elif progress >= ms2_goal and progress < ms3_goal:
  146. new_milestones = 2
  147. ms_reward = ms2_reward
  148. elif progress >= ms3_goal:
  149. new_milestones = 3
  150. ms_reward = ms3_reward
  151. else:
  152. new_milestones = 0
  153. if new_milestones > milestones: # If we're passing a milestone, get the reward
  154. cursor = db.execute(
  155. "UPDATE goals SET milestones = ? WHERE name = ?",
  156. (new_milestones, reward_name)
  157. )
  158. db.commit()
  159. current_app.logger.info(f'new_milestone: {new_milestones}') # TESTING
  160. if new_milestones < 3: # If there is another milestone
  161. next_milestone = new_milestones + 1 # Get the number of the next milestone
  162. next_milestone = f'milestone{next_milestone}' # Set up the dict entry name
  163. if next_milestone:
  164. next_milestone = milestones_info[next_milestone][0] # Get the name of the next milestone
  165. else:
  166. next_milestone = "Done"
  167. current_app.logger.info(f'The next milestone is \"{next_milestone}\"') # TESTING
  168. return ms_reward, next_milestone
  169. else:
  170. return False
  171. return False
  172. except Error as wmrerror:
  173. current_app.logger.error(f'Couldn\'t check if a milestone was reached: {wmrerror.args[0]}')
  174. return False
  175. def was_goal_reached(db, reward_name): # Check if a goal was reached
  176. try:
  177. cursor = db.execute(
  178. "SELECT progress, target FROM goals WHERE name = ?",
  179. (reward_name,)
  180. )
  181. row = cursor.fetchone()
  182. if row is None:
  183. current_app.logger.error(f'{reward_name} not found in Goal table.')
  184. else:
  185. progress, target = row
  186. if progress == target:
  187. cursor = db.execute(
  188. "UPDATE goals SET complete = TRUE WHERE name = ?",
  189. (reward_name,)
  190. )
  191. db.commit()
  192. return True
  193. return False
  194. except Error as wgrerror:
  195. current_app.logger.error(f'Couldn\'t mark goal met: {wgrerror.args[0]}')
  196. return False
  197. def all_votes(db): # Get all the votes
  198. try:
  199. cursor = db.execute(
  200. "SELECT votes.name, votes.count, votes.info FROM votes"
  201. )
  202. return cursor.fetchall()
  203. except Error as aterror:
  204. current_app.logger.error(f'Couldn\'t select all votes: {aterror.args[0]}')
  205. def refund_reward(db, reward_id): # Refund a user for a particular reward
  206. reward_id = reward_id
  207. try:
  208. cursor = db.execute(
  209. "UPDATE reward_queue SET refunded = 1 WHERE id = ?",
  210. (reward_id,)
  211. )
  212. db.commit()
  213. current_app.logger.info(f'Set {reward_id} as refunded in the database')
  214. return True
  215. except Error as rferror:
  216. current_app.logger.error(f'Couldn\'t refund reward id {reward_id}: {rferror.args[0]}')
  217. return False
  218. def fulfill_reward(db, reward_id): # Mark a reward as fulfilled in the database
  219. reward_id = reward_id
  220. try:
  221. cursor = db.execute(
  222. "UPDATE reward_queue SET fulfilled = 1 WHERE id = ?",
  223. (reward_id,)
  224. )
  225. db.commit()
  226. current_app.logger.info(f'{reward_id} marked fulfilled.')
  227. return True
  228. except Error as frerror:
  229. current_app.logger.error(f'Couldn\'t fulfill reward id {reward_id}: {frerror.args[0]}')
  230. return False
  231. def all_active_votes(db): # Get only active votes
  232. votes = all_votes(db)
  233. all_active_votes = []
  234. try:
  235. for name, count, info in votes:
  236. if is_reward_active(name):
  237. all_active_votes.append((name, count, info))
  238. return all_active_votes
  239. except Exception as aaverror:
  240. current_app.logger.error(f'General exception: {aaverror}')
  241. def all_goals(db): # Get all the goals
  242. try:
  243. cursor = db.execute(
  244. "SELECT name, progress, target, info FROM goals"
  245. )
  246. return cursor.fetchall()
  247. except Exception as agerror:
  248. current_app.logger.error(f'Couldn\'t select all goals: {agerror.args[0]}')
  249. def all_active_goals(db): # Get only active goals
  250. goals = all_goals(db)
  251. all_active_goals = []
  252. try:
  253. for name, progress, target, info in goals:
  254. if is_reward_active(name):
  255. all_active_goals.append((name, progress, target, info))
  256. return all_active_goals
  257. except Exception as aagerror:
  258. current_app.logger.error(f'General exception: {aagerror}')
  259. def all_active_rewards(): # Get only active rewards
  260. rewards = current_app.config['REWARDS']
  261. all_active_rewards = {}
  262. for reward_name, reward_dict in rewards.items():
  263. if reward_dict.get('categories'): # If reward has empty categories list
  264. for category in reward_dict['categories']: # Compare each category to ACTIVE_CAT list
  265. if category in current_app.config['ACTIVE_CAT']:
  266. all_active_rewards[reward_name] = reward_dict
  267. break
  268. return all_active_rewards
  269. def all_active_redeems():
  270. rewards = current_app.config['REWARDS']
  271. just_redeems = []
  272. for reward_name, details in rewards.items():
  273. if details.get("type") not in ["goal", "vote"]:
  274. just_redeems.append(reward_name)
  275. return just_redeems
  276. def save_alerts(alerts_dict): # Write alerts to alerts.py
  277. alerts_dict = json.dumps(alerts_dict, indent=4)
  278. alerts_file = os.path.join(current_app.instance_path, 'alerts.py')
  279. new_alerts = f"ALERTS = {alerts_dict}"
  280. try:
  281. with open(alerts_file, 'w') as af:
  282. af.write(new_alerts)
  283. af.close
  284. current_app.logger.info('Saved new alerts.py.')
  285. except Exception as saerror:
  286. current_app.logger.error(f'Couldn\'t save alerts.py: {saerror.args[0]}')
  287. return False
  288. current_app.config.from_pyfile('alerts.py', silent=True) # Reread alerts.py into the app
  289. return True
  290. def del_alert_file(alert_file):
  291. filepath = os.path.join(current_app.config['ASSETS_FOLDER'], alert_file)
  292. try:
  293. os.remove(filepath)
  294. current_app.logger.info(f"Successfully removed \"{alert_file}\" from alerts folder.")
  295. return True
  296. except FileNotFoundError:
  297. current_app.logger.error(f"Couldn't delet \"{alert_file}\": File not found.")
  298. return False
  299. except PermissionError:
  300. current_app.logger.error(f"No permission to delete file: \"{alert_file}\"")
  301. return False
  302. except Exception as daferror:
  303. current_app.logger.error(f"An error occurred: {daferror}")
  304. return False
  305. def save_rewards(reward_info): # Write rewards to rewards.py
  306. sorted_rewards = dict(sorted(reward_info.items(), key=sort_key))
  307. new_rewards = json.dumps(sorted_rewards, indent=4)
  308. rewards_file = os.path.join(current_app.instance_path, 'rewards.py')
  309. try:
  310. with open(rewards_file, 'w') as rf:
  311. rf.write(f'REWARDS = {new_rewards}')
  312. rf.close()
  313. except Exception as srerror:
  314. current_app.logger.error(f'Couldn\'t save rewards.py: {srerror.args[0]}')
  315. return False
  316. return True
  317. def save_config(config_dict): # Write settings to config.py
  318. settings_file = os.path.join(current_app.instance_path, 'config.py')
  319. secret_key = current_app.config['SECRET_KEY']
  320. new_settings = f"# Owncast stuff. Needed to interact with your Owncast instance\n\
  321. ACCESS_ID = '{config_dict['ACCESS_ID']}'\n\
  322. ACCESS_TOKEN = '{config_dict['ACCESS_TOKEN']}'\n\
  323. OWNCAST_URL = '{config_dict['OWNCAST_URL']}'\n\
  324. \n\
  325. # OwnchatBot Configuration \n\
  326. OCB_URL = '{config_dict['OCB_URL']}'\n\
  327. SECRET_KEY = '{secret_key}' # Needed for internal Flask stuff. DO NOT DELETE.\n\
  328. POINTS_INTERVAL = {config_dict['POINTS_INTERVAL']}\n\
  329. POINTS_AWARD = {config_dict['POINTS_AWARD']}\n\
  330. PREFIX = '{config_dict['PREFIX']}'"
  331. try:
  332. with open(settings_file, 'w') as cf:
  333. cf.write(new_settings)
  334. cf.close
  335. except Exception as scerror:
  336. current_app.logger.error(f'Couldn\'t save config.py: {scerror.args[0]}')
  337. return False
  338. current_app.config.from_pyfile('config.py', silent=True) # Reread config.py into the app
  339. return True
  340. def reread_categories(): # Read _CAT varaibles and write to categories.py
  341. categories_file = os.path.join(current_app.instance_path, 'categories.py')
  342. active_categories = current_app.config['ACTIVE_CAT']
  343. inactive_categories = current_app.config['INACTIVE_CAT']
  344. try:
  345. with open(categories_file, 'r', encoding='utf-8') as f: # Read categories.py, and set up lines to change
  346. category_data = f.readlines()
  347. category_data[0] = f'ACTIVE_CAT = {active_categories}\n'
  348. category_data[1] = f'INACTIVE_CAT = {inactive_categories}\n'
  349. f.close
  350. with open(categories_file, 'w', encoding='utf-8') as f: # Write changes to categories.py
  351. f.writelines(category_data)
  352. f.close
  353. current_app.config.from_pyfile('categories.py', silent=True) # Reread categories into the app
  354. except Error as rcerror:
  355. current_app.logger.error(f'Couldn\'t reread categories: {rcerror.args[0]}')
  356. def activate_category(category): # Move an item from the ACTIVE_CAT list to the INACTIVE_CAT list
  357. try:
  358. categories_file = os.path.join(current_app.instance_path, 'categories.py')
  359. active_categories = current_app.config['ACTIVE_CAT']
  360. inactive_categories = current_app.config['INACTIVE_CAT']
  361. active_categories.append(category) # Add to ACTIVE_CAT
  362. inactive_categories.remove(category) # Remove from INACTIVE_CAT
  363. reread_categories()
  364. except Error as acerror:
  365. current_app.logger.error(f'Couldn\'t activate {category}: {acerror.args[0]}')
  366. def deactivate_category(category): # Move an item from the INACTIVE_CAT list to the ACTIVE_CAT list
  367. try:
  368. categories_file = os.path.join(current_app.instance_path, 'categories.py')
  369. active_categories = current_app.config['ACTIVE_CAT']
  370. inactive_categories = current_app.config['INACTIVE_CAT']
  371. active_categories.remove(category) # Remove from ACTIVE_CAT
  372. inactive_categories.append(category) # Add to INACTIVE_CAT
  373. reread_categories()
  374. except Error as dcerror:
  375. current_app.logger.error(f'Couldn\'t deactivate {category}: {dcerror.args[0]}')
  376. def get_queue(db): # Get the reward queue and the username
  377. try:
  378. cursor = db.execute(
  379. """SELECT reward_queue.id, reward_queue.created, reward_queue.reward, reward_queue.user_id, reward_queue.fulfilled, reward_queue.refunded, points.name
  380. FROM reward_queue
  381. INNER JOIN points
  382. on reward_queue.user_id = points.id"""
  383. )
  384. return cursor.fetchall()
  385. except Error as gqerror:
  386. current_app.logger.error(f'Couldn\'t get queue: {gqerror.args[0]}')
  387. def is_reward_active(reward_name): # Check if reward is active
  388. active_categories = current_app.config['ACTIVE_CAT']
  389. reward_dict = current_app.config['REWARDS'].get(reward_name, None)
  390. try:
  391. if reward_dict:
  392. if 'categories' in reward_dict: # Is there a categories key at all?
  393. for category in reward_dict['categories']: # Cycle through categories and compare to active_categories
  394. if category in active_categories:
  395. return True
  396. return False
  397. elif reward_dict['categories'] == []: # If categories key is there but empty, return False
  398. return False
  399. else:
  400. return True
  401. return None
  402. except Error as iraerror:
  403. current_app.logger.error(f'Couldn\'t check if {reward_name} is active: {iraerror.args[0]}')