servers.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import pickle
  2. import os
  3. import math
  4. import asyncio
  5. from mcstatus import MinecraftServer
  6. import datetime
  7. #we use the ips of mc servers like ids of discord servers
  8. #Important note: DCServer objects must be created before MCServer objects
  9. client = None
  10. #these dictionaries store objects
  11. # with discord server ids/mc server ips as keys
  12. MCServers = {}
  13. DCServers = {}
  14. #this dictionaries store sets of mc server ips with
  15. # discord server id as keys and the other way around.
  16. linksMC_DC = {}
  17. linksDC_MC = {}
  18. def link(mc_ip, dc_id):
  19. try:
  20. linksMC_DC[mc_ip] = linksMC_DC[mc_ip].union([dc_id])
  21. except KeyError:
  22. linksMC_DC[mc_ip] = set([dc_id])
  23. try:
  24. linksDC_MC[dc_id] = linksDC_MC[dc_id].union([mc_ip])
  25. except KeyError:
  26. linksDC_MC[dc_id] = set([mc_ip])
  27. def get_time_for_next_update(count, threshold):
  28. if count < threshold / 2:
  29. return 15
  30. if count >= threshold:
  31. return 1
  32. return 5
  33. class MCServer:
  34. def __init__(self, ip, name, game_threshold, DCs):
  35. self.ip = ip
  36. self.name = name
  37. self.game_threshold = game_threshold
  38. MCServers[ip] = self
  39. self.time_since_last_update = math.inf
  40. self.count = 0
  41. for id in DCs:
  42. link(ip, id)
  43. #runs once per minute
  44. def update_tick(self):
  45. self.time_since_last_update += 1
  46. if self.time_since_last_update >= get_time_for_next_update(self.count, self.game_threshold):
  47. self.time_since_last_update = 0
  48. return self
  49. else:
  50. return None
  51. def set_count(self, count):
  52. changed = self.count == count
  53. self.count = count
  54. return changed
  55. class DCServer:
  56. def __init__(self, id, channelID):
  57. self.id = id
  58. self.channelID = channelID
  59. DCServers[id] = self
  60. def get_player_count_string(self):
  61. countstr = "Players Online: "
  62. for mc in linksDC_MC[self.id]:
  63. mc = MCServers[mc]
  64. if mc.count == -1:
  65. countstr += "Can't reach " + mc.name
  66. else:
  67. countstr += mc.name + " " + str(mc.count)
  68. if mc.count >= mc.game_threshold:
  69. countstr += "✳"
  70. countstr += " +++ "
  71. return countstr
  72. #Read servers and links from file if file exists
  73. try:
  74. dirname, filename = os.path.split(os.path.abspath(__file__))
  75. with open(os.path.join(dirname, "servers.ref"), "rb") as f:
  76. MCServers, DCServers, linksMC_DC, linksDC_MC = pickle.load(f)
  77. for server in MCServers:
  78. #update all servers on start
  79. MCServers[server].time_since_last_update = math.inf
  80. except FileNotFoundError:
  81. pass #use empty dictionaries as defined earlier
  82. #creates DCServer object if it doesn't exist yet
  83. def ensure_discord(id, channelID):
  84. try:
  85. DCServers[id] #already exists
  86. except KeyError:
  87. #create new DCServer object,
  88. # gets put into dict in constructor
  89. DCServer(id, channelID)
  90. def parse_arguments(command_content):
  91. words = command_content.split()
  92. return words[1], words[2], int(words[3])
  93. #returns response to message
  94. def add_mc_server(command):
  95. try:
  96. ip, name, threshold = parse_arguments(command.content)
  97. except IndexError:
  98. return "Could not instantiate Server object: Too few arguments"
  99. except ValueError:
  100. return "Could not instantiate Server object: The 3rd argument must be an integer"
  101. try:
  102. ensure_discord(command.guild.id, command.channel.id)
  103. except AttributeError:
  104. return "This command does not work in DM channels"
  105. try:
  106. MCServers[ip]
  107. #mc server already exists, link with discord
  108. try:
  109. if command.guild.id in linksMC_DC[ip]:
  110. return "already linked" #maybe make this unlink
  111. else:
  112. link(ip, command.guild.id)
  113. except KeyError:
  114. link(ip, command.guild.id)
  115. except KeyError:
  116. #create new mc server object
  117. MCServer(ip, name, threshold, [command.guild.id])
  118. #save servers to file
  119. dirname, filename = os.path.split(os.path.abspath(__file__))
  120. with open(os.path.join(dirname, "servers.ref"), "wb") as f:
  121. pickle.dump((MCServers, DCServers, linksMC_DC, linksDC_MC), f)
  122. return "Successfully linked MC Server"
  123. class Updater:
  124. mcServersToUpdate = set()
  125. dcServersToUpdate = set()
  126. force = False
  127. async def force_update(self, discordID):
  128. self.force = True
  129. mcs = linksDC_MC[discordID]
  130. for mc in mcs:
  131. mc = MCServers[mc]
  132. mc.time_since_last_update = math.inf
  133. self.update_tick()
  134. await self.wait_for_force()
  135. return DCServers[discordID].get_player_count_string()
  136. async def wait_for_force(self):
  137. while self.force:
  138. await asyncio.sleep(1)
  139. async def start(self):
  140. while True:
  141. now = datetime.datetime.now()
  142. after_minute = now.second + now.microsecond / 1_000_000
  143. if after_minute:
  144. await asyncio.sleep(60 - after_minute)
  145. self.update_tick()
  146. #called when all player counts are retrieved
  147. def done_updating(self):
  148. for update in self.updateResults:
  149. should_update_header, mcserver, _ = update.result()
  150. if should_update_header:
  151. self.dcServersToUpdate = self.dcServersToUpdate.union(linksMC_DC[mcserver.ip])
  152. for dc in self.dcServersToUpdate:
  153. dc = DCServers[dc]
  154. channel = client.get_channel(dc.channelID)
  155. if channel != None:
  156. loop = asyncio.get_event_loop()
  157. loop.create_task(channel.edit(topic = dc.get_player_count_string()))
  158. print("Updated player counter:", datetime.datetime.now())
  159. self.force = False
  160. #reSET these
  161. self.mcServersToUpdate = set()
  162. self.dcServersToUpdate = set()
  163. #called on the start of every minute
  164. def update_tick(self):
  165. for name in MCServers:
  166. s = MCServers[name].update_tick()
  167. if s != None:
  168. self.mcServersToUpdate = self.mcServersToUpdate.union([s])
  169. self.updatesToDo = len(self.mcServersToUpdate)
  170. self.updateResults = set()
  171. loop = asyncio.get_event_loop()
  172. def on_done(future):
  173. a, b, updater = future.result()
  174. updater.updatesToDo -= 1
  175. if updater.updatesToDo < 1:
  176. updater.done_updating()
  177. for server in self.mcServersToUpdate:
  178. f = loop.create_future()
  179. asyncio.ensure_future(self.update_server(f, server))
  180. f.add_done_callback(on_done)
  181. self.updateResults = self.updateResults.union([f])
  182. async def update_server(self, future, server):
  183. try:
  184. m_server = MinecraftServer.lookup(server.ip)
  185. count = m_server.status().players.online
  186. future.set_result((server.set_count(count), server, self))
  187. except:
  188. future.set_result((server.set_count(-1), server, self))