Experimental Discord bot written in Python
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

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