1
0

discord-bot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import json
  2. import sys
  3. import re
  4. import logging
  5. import discord
  6. import asyncio
  7. from datetime import datetime, timezone
  8. from config import Config
  9. from logger import logger
  10. from tinydb import Query
  11. import matplotlib.pyplot as plt
  12. import pandas as pd
  13. import marketMonitor
  14. from db import MarketDB, PlayerDB, _hist, getDF, NotificationDB, getMonitor
  15. TOKEN = Config["Tokens"]["BoobyLegendsEcon"]["Token"]
  16. def getPlayerCard(name):
  17. today=datetime.utcnow().strftime('%Y-%m-%d')
  18. thisCard=PlayerDB.search(Query().fragment({'date':today,'name':name}))[0]
  19. return thisCard
  20. def histGrid(name,columns=None):
  21. logger.debug(f"histGrid(name={name},columns={columns})")
  22. MAX_RECORD_COUNT=25
  23. df = getDF(name)
  24. footer=""
  25. dfSize=len(df)
  26. if dfSize > MAX_RECORD_COUNT:
  27. df.drop(df.head(dfSize-MAX_RECORD_COUNT).index,inplace=True)
  28. footer=f"\n{dfSize-MAX_RECORD_COUNT} more..."
  29. if columns is None:
  30. cols = ['date','cost','cost-min','cost-max']
  31. else:
  32. cols = columns
  33. return "```" + df[cols].to_string(index=False, col_space=6, na_rep="-") + footer + "```"
  34. def todayGrid(level=None,tier=None,columns=None):
  35. logger.debug(f"todayGrid(level={level},tier={tier},columns={columns})")
  36. MAX_RECORD_COUNT=21
  37. df = getDF(aDate=datetime.utcnow().strftime('%Y-%m-%d'))
  38. if level is not None:
  39. df=df[df['level']==level.lower()]
  40. if tier is not None:
  41. df=df[df['tier']==tier.upper()]
  42. footer=""
  43. dfSize=len(df)
  44. if dfSize > MAX_RECORD_COUNT:
  45. df.drop(df.tail(dfSize-MAX_RECORD_COUNT).index,inplace=True)
  46. footer=f"\n{dfSize-MAX_RECORD_COUNT} more..."
  47. if columns is None:
  48. cols = ['name','cost','cost-min','cost-max']
  49. else:
  50. cols = columns
  51. return "```" + df[cols].to_string(index=False, col_space=6, na_rep="-") + footer + "```"
  52. def save_notification(card, threshold, author):
  53. author_id = str(author.id)
  54. NotificationDB.insert({'card': card, 'threshold': threshold, 'author': author_id})
  55. def delete_notification(card, threshold, author):
  56. query = (Query()['card'] == card) & (Query()['threshold'] == threshold) & (Query()['author'] == str(author.id))
  57. result = NotificationDB.remove(query)
  58. return len(result) > 0
  59. async def periodic_check(client):
  60. while True:
  61. try:
  62. logger.debug("Starting periodic check of notifications.")
  63. notifications = NotificationDB.all()
  64. if len(NotificationDB) > 0:
  65. marketMonitor.main()
  66. logger.debug(f"Found {len(notifications)} notifications to check.")
  67. for notification in notifications:
  68. card = notification['card']
  69. threshold = notification['threshold']
  70. author = await client.fetch_user(notification['author'])
  71. logger.debug(f"Checking card: {card} for threshold: {threshold} by {author}")
  72. card_cost = getMonitor(card)
  73. if card_cost and int(card_cost) < threshold:
  74. retstr = f"{author.mention} The cost of {card} is below {threshold}!"
  75. logger.debug(retstr)
  76. await author.send(retstr)
  77. delete_notification(card, threshold, author)
  78. await asyncio.sleep(240) # Check every 4 minutes
  79. except Exception as e:
  80. logger.error(f"An error occurred in periodic_check: {e}")
  81. def load_notifications(client):
  82. notifications = NotificationDB.all()
  83. logger.debug(f"Loading {len(notifications)} notifications.")
  84. for notification in notifications:
  85. card = notification['card']
  86. threshold = notification['threshold']
  87. author = notification['author']
  88. logger.debug(f"Loaded notification for card: {card} with threshold: {threshold} for author: {author}.")
  89. async def notify_below_threshold(card, threshold, author, client):
  90. try:
  91. logger.info(f"Notification requested for: {card} if cost is below {threshold}")
  92. marketMonitor.main()
  93. card_cost = getMonitor(card)
  94. print(f"{card} costs {card_cost}")
  95. if card_cost is not None:
  96. card_cost = int(card_cost)
  97. logger.debug(f"Card cost for {card}: {card_cost}")
  98. if card_cost < threshold:
  99. retstr = f"{author.mention} The cost of {card} is already below {threshold}!"
  100. return retstr # Return immediately when condition is met
  101. else:
  102. retstr = f"I'll let you know if the cost of {card} drops below {threshold}! (remove notify request with **!delnotify {card} {threshold}**)"
  103. save_notification(card, threshold, author)
  104. return retstr # Return initial message if condition not met
  105. else:
  106. retstr = f"Could not find data for {card}."
  107. logger.debug(retstr)
  108. except IndexError:
  109. retstr = f"Could not find data for {card}."
  110. logger.debug(retstr)
  111. except Exception as e:
  112. logger.error(f"An error occurred: {e}")
  113. def postPlot(card=None, columns=['cost', 'cost-min', 'cost-max']):
  114. df = getDF(card).plot(x='date', y=columns)
  115. plt.title(str(card))
  116. plt.xticks(rotation=45)
  117. plt.xlabel("Date")
  118. plt.ylabel("Cost")
  119. plt.savefig(f"plots/{card}.png")
  120. with open(f"plots/{card}.png", 'rb') as f:
  121. picture = discord.File(f)
  122. return picture
  123. def getData():
  124. d=MarketDB.all()
  125. df=pd.DataFrame(d)
  126. df.sort_values(by=['card-num','date'], ascending=[True,True], inplace=True)
  127. df.to_csv('plots/BoobyLegendsMarket.csv',index=False)
  128. with open(f"plots/BoobyLegendsMarket.csv", 'rb') as f:
  129. data = discord.File(f)
  130. return data
  131. async def parse(input, author):
  132. parts = input.strip().split()
  133. logger.debug(input)
  134. logger.debug(parts)
  135. AuthorName = str(author)
  136. if len(parts) == 0 or parts[0][0] not in ['!', '/', '\\']:
  137. logger.debug("Not a command")
  138. return None
  139. try:
  140. command = parts[0][1:].upper()
  141. logger.debug("Command: " + command)
  142. # Process different commands
  143. if command == "HISTORY":
  144. name = ' '.join(parts[1:])
  145. logger.info("History for: " + name)
  146. history = histGrid(name.strip())
  147. retstr = "{Author} requested history for **{name}**:\n{history}".format(
  148. Author=AuthorName,
  149. name=name,
  150. history=history
  151. )
  152. logger.debug(retstr)
  153. elif command == "TODAY":
  154. level = parts[1].lower() if len(parts) > 1 else None
  155. tier = parts[2].upper() if len(parts) > 2 else None
  156. logger.info(f"Today {level} {tier}")
  157. history = todayGrid(level, tier)
  158. retstr = "{Author} requested today's Costs for Today:\n{history}".format(
  159. Author=AuthorName,
  160. history=history
  161. )
  162. logger.debug(retstr)
  163. elif command == "PLOT":
  164. name = ' '.join(parts[1:])
  165. logger.info("Plot for: " + name)
  166. retstr = postPlot(card=name.strip())
  167. logger.debug(f"Plot created for {name}")
  168. elif command == "GETDATA":
  169. logger.info("Getting Data Dump")
  170. retstr = getData()
  171. logger.debug("Data Dump requested")
  172. elif command == "LOGLEVEL":
  173. if parts[1].upper() == "DEBUG":
  174. logger.setLevel(logging.DEBUG)
  175. elif parts[1].upper() == "INFO":
  176. logger.setLevel(logging.INFO)
  177. retstr = f"LogLevel set to {parts[1].upper()}"
  178. logger.debug(retstr)
  179. elif command == "HELP":
  180. retstr = '''
  181. My keywords are **!**, **/**, or **\\**
  182. Show a Card's Historical cost with: **!History Eva Elfie**
  183. Show the current day's average prices with:**!Today**
  184. You can also filter this by Level or Level and tier: **!Today rare** or **!Today common S**
  185. You can get a graph plotted with the historical values for a star by: **!Plot Natasha Nice**
  186. Get a Data Dump of the current data set with: **!GetData**
  187. Get notified when a card's cost drops below a certain threshold with: **!Notify Eva Elfie 100**
  188. '''
  189. logger.debug("Help command requested")
  190. elif command == "NOTIFY":
  191. try:
  192. card = ' '.join(parts[1:-1])
  193. threshold = int(parts[-1])
  194. if isinstance(author, str):
  195. author = await client.fetch_user(author)
  196. response = await notify_below_threshold(card, threshold, author, client)
  197. return response
  198. except ValueError:
  199. retstr = "Please provide a valid threshold number."
  200. logger.error("Invalid threshold number provided")
  201. return retstr
  202. except IndexError:
  203. retstr = "Please provide both a card name and a threshold number."
  204. logger.error("Card name and threshold not provided")
  205. return retstr
  206. elif command == "DELNOTIFY":
  207. try:
  208. card = ' '.join(parts[1:-1])
  209. threshold = int(parts[-1])
  210. if isinstance(author, str):
  211. author = await client.fetch_user(author)
  212. if delete_notification(card, threshold, author):
  213. retstr = f"Notification for {card} at threshold {threshold} has been removed."
  214. else:
  215. retstr = f"No notification found for {card} at threshold {threshold} by {author.mention}."
  216. logger.info(retstr)
  217. return retstr
  218. except ValueError:
  219. retstr = "Please provide a valid threshold number."
  220. logger.error("Invalid threshold number provided")
  221. return retstr
  222. except IndexError:
  223. retstr = "Please provide both a card name and a threshold number."
  224. logger.error("Card name and threshold not provided")
  225. return retstr
  226. else:
  227. retstr = f"{AuthorName}, your command '{command}' was not understood."
  228. logger.debug(f"Unknown command: {command}")
  229. except Exception as e:
  230. logger.error(f"An error occurred: {e}")
  231. retstr = None
  232. return retstr
  233. #########################################################################
  234. # Main Program
  235. #########################################################################
  236. intents = discord.Intents.default()
  237. intents.message_content = True
  238. client = discord.Client(intents=intents)
  239. @client.event
  240. async def on_message(message):
  241. # Check if the message is from the specific channel
  242. if str(message.channel.id) != "1187032500657201162":
  243. return
  244. if message.author == client.user:
  245. return
  246. output = await parse(message.content, message.author)
  247. if output is not None:
  248. if isinstance(output, str):
  249. await message.channel.send(output)
  250. else:
  251. await message.channel.send(file=output)
  252. @client.event
  253. async def on_ready():
  254. print('Logged in as')
  255. print(client.user.name)
  256. print(client.user.id)
  257. print('------')
  258. load_notifications(client) # Load and log notifications on startup
  259. client.loop.create_task(periodic_check(client)) # Start the periodic check task
  260. client.run(TOKEN)