reward_handlers.py 17 KB

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