Experimental Discord bot written in Python
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

storage.py 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """
  2. Handles storage of persisted and non-persisted data for the bot.
  3. """
  4. import json
  5. from os.path import exists
  6. from discord import Guild
  7. from config import CONFIG
  8. class ConfigKey:
  9. """
  10. Common keys in persisted guild storage.
  11. """
  12. WARNING_CHANNEL_ID = 'warning_channel_id'
  13. WARNING_MENTION = 'warning_mention'
  14. class Storage:
  15. """
  16. Static class for managing persisted bot configuration and transient state
  17. on a per-guild basis.
  18. """
  19. # -- Transient state management -----------------------------------------
  20. __guild_id_to_state = {}
  21. @classmethod
  22. def get_state(cls, guild: Guild) -> dict:
  23. """
  24. Returns transient state for the given guild. This state is not preserved
  25. if the bot is restarted.
  26. """
  27. state: dict = cls.__guild_id_to_state.get(guild.id)
  28. if state is None:
  29. state = {}
  30. cls.__guild_id_to_state[guild.id] = state
  31. return state
  32. @classmethod
  33. def get_state_value(cls, guild: Guild, key: str):
  34. """
  35. Returns a state value for the given guild and key, or `None` if not set.
  36. """
  37. return cls.get_state(guild).get(key)
  38. @classmethod
  39. def set_state_value(cls, guild: Guild, key: str, value) -> None:
  40. """
  41. Updates a transient value associated with the given guild and key name.
  42. A value of `None` removes any previous value for that key.
  43. """
  44. cls.set_state_values(guild, { key: value })
  45. @classmethod
  46. def set_state_values(cls, guild: Guild, values: dict) -> None:
  47. """
  48. Merges in a set of key-value pairs into the transient state for the
  49. given guild. Any pairs with a value of `None` will be removed from the
  50. transient state.
  51. """
  52. if values is None or len(values) == 0:
  53. return
  54. state: dict = cls.get_state(guild)
  55. for key, value in values.items():
  56. if value is None:
  57. del state[key]
  58. else:
  59. state[key] = value
  60. # XXX: Superstitious. Should update by ref already but saw weirdness once.
  61. cls.__guild_id_to_state[guild.id] = state
  62. # -- Persisted configuration management ---------------------------------
  63. # discord.Guild.id -> dict
  64. __guild_id_to_config = {}
  65. @classmethod
  66. def get_config(cls, guild: Guild) -> dict:
  67. """
  68. Returns all persisted configuration for the given guild.
  69. """
  70. config: dict = cls.__guild_id_to_config.get(guild.id)
  71. if config is not None:
  72. # Already in memory
  73. return config
  74. # Load from disk if possible
  75. cls.__trace(f'No loaded config for guild {guild.id}. Attempting to ' +
  76. 'load from disk.')
  77. config = cls.__read_guild_config(guild)
  78. if config is None:
  79. return {}
  80. cls.__guild_id_to_config[guild.id] = config
  81. return config
  82. @classmethod
  83. def get_config_value(cls, guild: Guild, key: str):
  84. """
  85. Returns a persisted guild config value stored under the given key.
  86. Returns `None` if not present.
  87. """
  88. return cls.get_config(guild).get(key)
  89. @classmethod
  90. def set_config_value(cls, guild: Guild, key: str, value) -> None:
  91. """
  92. Adds/updates the given key-value pair to the persisted config for the
  93. given Guild. If `value` is `None` the key will be removed from persisted
  94. config.
  95. """
  96. cls.set_config_values(guild, { key: value })
  97. @classmethod
  98. def set_config_values(cls, guild: Guild, values: dict) -> None:
  99. """
  100. Merges the given `values` dict with the saved config for the given guild
  101. and writes it to disk. `values` must be JSON-encodable or a `ValueError`
  102. will be raised. Keys with associated values of `None` will be removed
  103. from the persisted config.
  104. """
  105. if values is None or len(values) == 0:
  106. return
  107. config: dict = cls.get_config(guild)
  108. try:
  109. json.dumps(values)
  110. except Exception as e:
  111. raise ValueError(f'values not JSON encodable - {values}') from e
  112. for key, value in values.items():
  113. if value is None:
  114. del config[key]
  115. else:
  116. config[key] = value
  117. cls.__write_guild_config(guild, config)
  118. @classmethod
  119. def __write_guild_config(cls, guild: Guild, config: dict) -> None:
  120. """
  121. Saves config for a guild to a JSON file on disk.
  122. """
  123. path: str = cls.__guild_config_path(guild)
  124. cls.__trace(f'Saving config for guild {guild.id} to {path}')
  125. cls.__trace(f'config = {config}')
  126. with open(path, 'w', encoding='utf8') as file:
  127. # Pretty printing to make more legible for debugging
  128. # Sorting keys to help with diffs
  129. json.dump(config, file, indent='\t', sort_keys=True)
  130. cls.__trace('State saved')
  131. @classmethod
  132. def __read_guild_config(cls, guild: Guild) -> dict:
  133. """
  134. Loads config for a guild from a JSON file on disk, or `None` if not
  135. found.
  136. """
  137. path: str = cls.__guild_config_path(guild)
  138. if not exists(path):
  139. cls.__trace(f'No config on disk for guild {guild.id}. Returning None.')
  140. return None
  141. cls.__trace(f'Loading config from disk for guild {guild.id}')
  142. with open(path, 'r', encoding='utf8') as file:
  143. config = json.load(file)
  144. cls.__trace('State loaded')
  145. return config
  146. @classmethod
  147. def __guild_config_path(cls, guild: Guild) -> str:
  148. """
  149. Returns the JSON file path where guild config should be written.
  150. """
  151. config_value: str = CONFIG['config_path']
  152. path: str = config_value if config_value.endswith('/') else f'{config_value}/'
  153. return f'{path}guild_{guild.id}.json'
  154. @classmethod
  155. def __trace(cls, message: str) -> None:
  156. # print(f'{cls.__name__}: {message}')
  157. pass