Experimental Discord bot written in Python
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

storage.py 4.9KB

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