A RedBot cog for sending notifications when there are new founderless regions in NationStates.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 line
7.5 KiB

  1. from redbot.core import commands, checks, Config
  2. import discord
  3. import asyncio
  4. import requests
  5. import xml.etree.cElementTree as et
  6. import time
  7. import datetime
  8. import gzip
  9. R_HEADERS = {'User-Agent': '<Nation: Haku>'}
  10. RATE_LIMIT = 0.7
  11. def canonicalize(in_str):
  12. return in_str.lower().replace(' ', '_')
  13. def get_founderless_regions():
  14. """Return the list of founderless regions."""
  15. r = requests.get('https://www.nationstates.net/cgi-bin/api.cgi?q=regionsbytag;tags=founderless',
  16. headers=R_HEADERS)
  17. time.sleep(RATE_LIMIT)
  18. tree = et.fromstring(r.text)
  19. return tree[0].text.split(',')
  20. def download_region_dump():
  21. """Download the latest region dump from the NS API."""
  22. r = requests.get('https://www.nationstates.net/pages/regions.xml.gz',
  23. headers=R_HEADERS)
  24. time.sleep(RATE_LIMIT)
  25. with open('regions.xml.gz', 'wb') as f:
  26. f.write(r.content)
  27. with gzip.open('regions.xml.gz', 'rb') as f_in:
  28. with open('regions.xml', 'wb') as f_out:
  29. f_out.write(f_in.read())
  30. def get_region_endos(regions):
  31. """Using the region dump, return a Dict with the number of endos for each region."""
  32. tree = et.parse('regions.xml')
  33. endo_dict = dict()
  34. root = tree.getroot()
  35. for region in root:
  36. if region[0].text in regions:
  37. endo_dict[region[0].text] = int(region[5].text) - 1
  38. return endo_dict
  39. class FounderlessNotify(commands.Cog):
  40. """Notifies when a region changes in founderless status."""
  41. def __init__(self, bot, *args, **kwargs):
  42. super().__init__(*args, **kwargs)
  43. self.bot = bot
  44. self.config = Config.get_conf(self, identifier=82732344, force_registration=True)
  45. default_global_settings = {'update_time': 0, 'previous_founderless': [], 'notify_channel': 0}
  46. self.config.register_global(**default_global_settings)
  47. self.bg_loop_task = asyncio.create_task(self.bg_loop())
  48. def cog_unload(self):
  49. if self.bg_loop_task:
  50. self.bg_loop_task.cancel()
  51. @commands.command()
  52. @checks.is_owner()
  53. async def is_task_running(self, ctx):
  54. """Check if the main loop is running."""
  55. if self.bg_loop_task:
  56. await ctx.send('True')
  57. else:
  58. await ctx.send('False')
  59. @commands.command()
  60. @checks.is_owner()
  61. async def start_task(self, ctx):
  62. """Start the main loop if it is not running."""
  63. if self.bg_loop_task:
  64. await ctx.send('Task is already running')
  65. else:
  66. self.bg_loop_task = asyncio.create_task(self.bg_loop())
  67. @commands.command()
  68. @checks.is_owner()
  69. async def force_check(self, ctx, update_type):
  70. """Force perform a check in the differences of founderless regions."""
  71. if update_type.lower() == 'major' or update_type.lower() == 'minor':
  72. await self.update_founderless(update_type)
  73. else:
  74. return
  75. async def update_founderless(self, update_type):
  76. """Check for the differences in the founderless regions and then output that to a channel."""
  77. channel_id = await self.config.notify_channel()
  78. channel = self.bot.get_channel(channel_id)
  79. await channel.send(f'Beginning {update_type} check...')
  80. new_founderless_regions = get_founderless_regions()
  81. previous_founderless_regions = await self.config.previous_founderless()
  82. # A region is now foundered if it was in the previous list, but isn't in the current list
  83. # now_foundered = [region for region in previous_founderless_regions if (region not in new_founderless_regions)]
  84. # A region is now founderless if it is in the current list, but wasn't in the previous list
  85. now_founderless = [region for region in new_founderless_regions if (region not in previous_founderless_regions)]
  86. # Get the region endos for both lists
  87. # now_foundered_endos = get_region_endos(now_foundered)
  88. now_founderless_endos = get_region_endos(now_founderless)
  89. message_content = ''
  90. await channel.send('The following regions are now **Founderless**:')
  91. for region in now_founderless:
  92. if len(message_content) <= 1800:
  93. message_content += f'https://www.nationstates.net/region={canonicalize(region)} ' \
  94. f'({now_founderless_endos[region]}e)\n'
  95. else:
  96. await channel.send(message_content)
  97. message_content = ""
  98. message_content += f'https://www.nationstates.net/region={canonicalize(region)} ' \
  99. f'({now_founderless_endos[region]}e)\n'
  100. # Send the remaining contents just in case
  101. if message_content:
  102. await channel.send(message_content)
  103. await self.config.previous_founderless.set(new_founderless_regions)
  104. async def bg_loop(self):
  105. """Main background loop."""
  106. while True:
  107. # Only check once every 10 minutes
  108. await asyncio.sleep(600)
  109. current_time = datetime.datetime.utcnow()
  110. major_time = await self.config.update_time()
  111. minor_time = major_time + 12
  112. # Major Update
  113. if current_time.hour == (major_time + 2):
  114. download_region_dump()
  115. await self.update_founderless('major')
  116. await asyncio.sleep(3600)
  117. # Minor Update
  118. elif current_time.hour == (minor_time + 1):
  119. await self.update_founderless('minor')
  120. await asyncio.sleep(3600)
  121. @commands.command()
  122. @checks.is_owner()
  123. async def update_channel(self, ctx):
  124. """Set the notification channel."""
  125. await self.config.notify_channel.set(ctx.channel.id)
  126. await ctx.send(f'Founderless notify channel set to {ctx.channel.mention}')
  127. @commands.command()
  128. @checks.is_owner()
  129. async def update_time(self, ctx, update):
  130. """Set the update time."""
  131. update = int(update)
  132. await self.config.update_time.set(update)
  133. await ctx.send(f'Major update set to: {await self.config.update_time()}:00 UTC'
  134. f'\nMinor update set to: {await self.config.update_time() + 12}:00 UTC')
  135. @commands.command()
  136. @checks.is_owner()
  137. async def get_settings(self, ctx):
  138. """Get the current settings from config."""
  139. stored_channel = await self.config.notify_channel()
  140. major_time = await self.config.update_time()
  141. minor_time = major_time + 12
  142. founderless = await self.config.previous_founderless()
  143. channel = ctx.guild.get_channel(stored_channel)
  144. await ctx.send(f'Current Channel: {channel.mention}\nCurrent Major Update Time: {major_time}:00 UTC\n'
  145. f'Current Minor Update Time: {minor_time}:00 UTC\nCurrent # Founderless: {len(founderless)}')
  146. @commands.command()
  147. @checks.is_owner()
  148. async def force_update_founderless(self, ctx):
  149. """Manually update the founderless region list."""
  150. founderless_regions = get_founderless_regions()
  151. await self.config.previous_founderless.set(founderless_regions)
  152. await ctx.send(f'Manually updated founderless regions list. There are now '
  153. f'{len(founderless_regions)} founderless regions.')
  154. @commands.command()
  155. @checks.is_owner()
  156. async def force_download_dump(self, ctx):
  157. """Manually download the region dump."""
  158. await ctx.send('Downloading region dump...')
  159. download_region_dump()
  160. await ctx.send('Region dump successfully updated.')