1 /** 2 Base class for creating plugins the Bot can load/unload. 3 */ 4 5 module dcord.bot.plugin; 6 7 import std.path, 8 std.file, 9 std.variant; 10 11 import std.experimental.logger, 12 vibe.core.core : runTask; 13 14 import dcord.bot, 15 dcord.types, 16 dcord.client, 17 dcord.util.storage; 18 19 /** 20 Sentinel for @Synced attributes 21 TODO: this is messy. Better way to achieve a sentinel? 22 */ 23 struct SyncedAttribute { 24 string syncedAttributeSentinel; 25 }; 26 27 /** 28 UDA which tells StateSyncable that a member attribute should be synced into 29 the state on plugin load/unload. 30 */ 31 SyncedAttribute Synced() { 32 return SyncedAttribute(); 33 } 34 35 /** 36 PluginState is a class that encapsulates all run-time states required for a 37 plugin to exist. It's purpose is to allow for hot-reloading and replacing 38 of plugin code, without having to destroy or rebuild run-time data. 39 */ 40 class PluginState { 41 /** Plugin JSON Storage file (for data) */ 42 Storage storage; 43 44 /** Plugin JSON Config file */ 45 Storage config; 46 47 /** PluginOptions struct */ 48 PluginOptions options; 49 50 /** Custom state data stored by the plugin */ 51 Variant[string] custom; 52 53 this(Plugin plugin, PluginOptions opts) { 54 this.options = opts ? opts : new PluginOptions; 55 if (this.options.useStorage) this.storage = new Storage(plugin.storagePath); 56 if (this.options.useConfig) this.config = new Storage(plugin.configPath); 57 } 58 } 59 60 /** 61 The StateSyncable template is an implementation which handles the syncing of 62 member attributes into are PluginState.custom store during plugin load/unload. 63 This allows plugin developers to simply attach the @Synced UDA to any attributes 64 they wish to be stored, and then call stateLoad and stateUnload in the plugin 65 load/unload functions. 66 */ 67 mixin template StateSyncable() { 68 /// Loads all custom attribute state from a PluginState. 69 void stateLoad(T)(PluginState state) { 70 foreach (mem; __traits(allMembers, T)) { 71 foreach (attr; __traits(getAttributes, __traits(getMember, T, mem))) { 72 static if(__traits(hasMember, attr, "syncedAttributeSentinel")) { 73 if (mem in state.custom && state.custom[mem].hasValue()) { 74 mixin("(cast(T)this)." ~ mem ~ " = " ~ "state.custom[\"" ~ mem ~ "\"].get!(typeof(__traits(getMember, T, mem)));"); 75 } 76 } 77 } 78 } 79 } 80 81 /// Unloads all custom attributes into a PluginState. 82 void stateUnload(T)(PluginState state) { 83 foreach (mem; __traits(allMembers, T)) { 84 foreach (attr; __traits(getAttributes, __traits(getMember, T, mem))) { 85 static if(__traits(hasMember, attr, "syncedAttributeSentinel")) { 86 mixin("state.custom[\"" ~ mem ~ "\"] = " ~ "Variant((cast(T)this)." ~ mem ~ ");"); 87 } 88 } 89 } 90 } 91 } 92 93 /** 94 PluginOptions is a class that can be used to configure the base functionality 95 and utilties in use by a plugin. 96 */ 97 class PluginOptions { 98 /** Does this plugin load/require a configuration file? */ 99 bool useConfig = false; 100 101 /** Does this plugin load/require a JSON storage file? */ 102 bool useStorage = false; 103 104 /** Does this plugin auto-load level/command overrides from its config? */ 105 bool useOverrides = false; 106 107 /** Default command group to use */ 108 string commandGroup = ""; 109 } 110 111 /** 112 A Plugin represents a modular, extendable class that encapsulates certain 113 Bot functionality into a logical slice. Plugins usually have a set of commands 114 and listeners attached to them, and are built to be dynamically loaded/reloaded 115 into a Bot. 116 */ 117 class Plugin { 118 /// Bot instance for this plugin. Should always be set 119 Bot bot; 120 121 /// Current runtime state for this plugin 122 PluginState state; 123 124 mixin Listenable; 125 mixin Commandable; 126 mixin StateSyncable; 127 128 /** 129 The path to the dynamic library this plugin was loaded from. If set, this 130 signals this Plugin was loaded from a dynamic library, and can be reloaded 131 from the given path. 132 */ 133 string dynamicLibraryPath; 134 135 136 /// Constructor for initial load. Usually called from the inherited constructor. 137 this(this T)(PluginOptions opts = null) { 138 this.state = new PluginState(this, opts); 139 140 this.loadCommands!T(); 141 this.loadListeners!T(); 142 } 143 144 /// Plugin log instance. 145 @property Logger log() { 146 return this.bot.log; 147 } 148 149 /// Used to load the Plugin, initially loading state if requred. 150 void load(Bot bot, PluginState state = null) { 151 this.bot = bot; 152 153 // Make sure our storage directory exists 154 if(this.options.useStorage && !exists(this.storageDirectoryPath)) { 155 mkdirRecurse(this.storageDirectoryPath); 156 } 157 158 // If we got state, assume this was a plugin reload and replace 159 if(state) { 160 this.state = state; 161 } else { 162 // If plugin uses storage, load the storage from disk 163 if (this.options.useStorage) { 164 this.storage.load(); 165 this.storage.save(); 166 } 167 168 // If plugin uses config, load the config from disk 169 if(this.options.useConfig) { 170 this.config.load(); 171 this.config.save(); 172 } 173 } 174 175 string group = this.options.commandGroup; 176 177 if (this.options.useOverrides && this.config) { 178 if (this.config.has("levels")) { 179 auto levels = this.config.get!(VibeJSON[string])("levels"); 180 foreach (name, level; levels) { 181 auto cmd = this.commands[name]; 182 cmd.level = level.get!int; 183 } 184 } 185 186 // Try grabbing an override for group 187 group = this.config.get!string("group", group); 188 } 189 190 // If we have an override value for the commandgroup, set it now on all commands 191 if(group != "") 192 foreach(command; this.commands.values) command.setGroup(group); 193 } 194 195 /// Used to unload the Plugin. Saves config/storage if required. 196 void unload(Bot bot) { 197 if(this.options.useStorage) this.storage.save(); 198 if(this.options.useConfig) this.config.save(); 199 } 200 201 /// Returns path to this plugins storage directory. 202 @property string storageDirectoryPath() { 203 return "storage" ~ dirSeparator ~ this.name; 204 } 205 206 /// Returns path to this plugins storage file. 207 @property string storagePath() { 208 return this.storageDirectoryPath ~ dirSeparator ~ "storage.json"; 209 } 210 211 /// Returns path to this plugins config file. 212 @property string configPath() { 213 return "config" ~ dirSeparator ~ this.name ~ ".json"; 214 } 215 216 /// Storage instance for this plugin. 217 @property Storage storage() { 218 return this.state.storage; 219 } 220 221 /// Config instance for this plugin 222 @property Storage config() { 223 return this.state.config; 224 } 225 226 /// PluginOptions for this plugin 227 @property PluginOptions options() { 228 return this.state.options; 229 } 230 231 /// Client instance for the Bot running this plugin 232 @property Client client() { 233 return this.bot.client; 234 } 235 236 /// User instance for the account this bot is running under 237 @property User me() { 238 return this.client.state.me; 239 } 240 241 /// Returns the name of this plugin. 242 string name() { 243 return typeof(this).toString; 244 } 245 }