1 /++ 2 A simple stopwatch plugin. It offers the ability to start and stop timers, 3 to get how much time passed between the creation of a stopwatch and the 4 cessation of it. 5 6 See_Also: 7 https://github.com/zorael/kameloso/wiki/Current-plugins#stopwatch, 8 [kameloso.plugins.common.core], 9 [kameloso.plugins.common.misc] 10 11 Copyright: [JR](https://github.com/zorael) 12 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 13 14 Authors: 15 [JR](https://github.com/zorael) 16 +/ 17 module kameloso.plugins.stopwatch; 18 19 version(WithStopwatchPlugin): 20 21 private: 22 23 import kameloso.plugins; 24 import kameloso.plugins.common.core; 25 import kameloso.plugins.common.awareness : MinimalAuthentication; 26 import kameloso.messaging; 27 import dialect.defs; 28 import std.typecons : Flag, No, Yes; 29 30 31 // StopwatchSettings 32 /++ 33 All Stopwatch plugin runtime settings aggregated. 34 +/ 35 @Settings struct StopwatchSettings 36 { 37 /// Whether or not this plugin is enabled. 38 @Enabler bool enabled = true; 39 } 40 41 42 // onCommandStopwatch 43 /++ 44 Manages stopwatches. 45 +/ 46 @(IRCEventHandler() 47 .onEvent(IRCEvent.Type.CHAN) 48 .permissionsRequired(Permissions.whitelist) 49 .channelPolicy(ChannelPolicy.home) 50 .addCommand( 51 IRCEventHandler.Command() 52 .word("stopwatch") 53 .policy(PrefixPolicy.prefixed) 54 .description("Starts, stops, or shows status of stopwatches.") 55 .addSyntax("$command start") 56 .addSyntax("$command stop") 57 .addSyntax("$command status") 58 ) 59 .addCommand( 60 IRCEventHandler.Command() 61 .word("sw") 62 .policy(PrefixPolicy.prefixed) 63 .hidden(true) 64 ) 65 ) 66 void onCommandStopwatch(StopwatchPlugin plugin, const ref IRCEvent event) 67 { 68 import lu.string : nom, stripped, strippedLeft; 69 import std.datetime.systime : Clock, SysTime; 70 import std.format : format; 71 72 void sendUsage() 73 { 74 enum pattern = "Usage: <b>%s%s<b> [start|stop|status]"; // hide clear 75 immutable message = pattern.format(plugin.state.settings.prefix, event.aux[$-1]); 76 chan(plugin.state, event.channel, message); 77 } 78 79 void sendNoStopwatch() 80 { 81 enum message = "You do not have a stopwatch running."; 82 chan(plugin.state, event.channel, message); 83 } 84 85 void sendNoSuchStopwatch(const string id) 86 { 87 enum pattern = "There is no such stopwatch running. (<h>%s<h>)"; 88 immutable message = pattern.format(id); 89 chan(plugin.state, event.channel, message); 90 } 91 92 void sendCannotStopOthersStopwatches() 93 { 94 enum message = "You cannot end or stop someone else's stopwatch."; 95 chan(plugin.state, event.channel, message); 96 } 97 98 void sendStoppedAfter(const string diff) 99 { 100 enum pattern = "Stopwatch stopped after <b>%s<b>."; 101 immutable message = pattern.format(diff); 102 chan(plugin.state, event.channel, message); 103 } 104 105 void sendElapsedTime(const string diff) 106 { 107 enum pattern = "Elapsed time: <b>%s<b>"; 108 immutable message = pattern.format(diff); 109 chan(plugin.state, event.channel, message); 110 } 111 112 void sendMissingClearPermissions() 113 { 114 enum message = "You do not have permissions to clear all stopwatches."; 115 chan(plugin.state, event.channel, message); 116 } 117 118 void sendClearingStopwatches(const string channelName) 119 { 120 enum pattern = "Clearing all stopwatches in channel <b>%s<b>."; 121 immutable message = pattern.format(channelName); 122 chan(plugin.state, event.channel, message); 123 } 124 125 void sendStartedOrRestarted(const bool restarted) 126 { 127 immutable message = "Stopwatch " ~ (restarted ? "restarted!" : "started!"); 128 chan(plugin.state, event.channel, message); 129 } 130 131 string slice = event.content.stripped; // mutable 132 immutable verb = slice.nom!(Yes.inherit)(' '); 133 slice = slice.strippedLeft; 134 135 string getDiff(const string id) 136 { 137 import kameloso.time : timeSince; 138 import core.time : msecs; 139 140 auto channelWatches = event.channel in plugin.stopwatches; 141 assert(channelWatches, "Tried to access stopwatches from nonexistent channel"); 142 143 auto watch = id in *channelWatches; 144 assert(watch, "Tried to fetch stopwatch start timestamp for a nonexistent id"); 145 146 auto now = Clock.currTime; 147 now.fracSecs = 0.msecs; 148 immutable diff = now - SysTime.fromUnixTime(*watch); 149 return timeSince(diff); 150 } 151 152 switch (verb) 153 { 154 case "start": 155 auto channelWatches = event.channel in plugin.stopwatches; 156 immutable stopwatchAlreadyExists = (channelWatches && (event.sender.nickname in *channelWatches)); 157 plugin.stopwatches[event.channel][event.sender.nickname] = Clock.currTime.toUnixTime; 158 return sendStartedOrRestarted(stopwatchAlreadyExists); 159 160 case "stop": 161 case "end": 162 case "status": 163 case string.init: 164 immutable id = slice.length ? 165 slice : 166 event.sender.nickname; 167 168 auto channelWatches = event.channel in plugin.stopwatches; 169 if (!channelWatches || (id !in *channelWatches)) 170 { 171 return (id == event.sender.nickname) ? 172 sendNoStopwatch() : 173 sendNoSuchStopwatch(id); 174 } 175 176 immutable diff = getDiff(id); 177 178 switch (verb) 179 { 180 case "stop": 181 case "end": 182 if ((id != event.sender.nickname) && (event.sender.class_ < IRCUser.Class.operator)) 183 { 184 return sendCannotStopOthersStopwatches(); 185 } 186 187 plugin.stopwatches[event.channel].remove(id); 188 return sendStoppedAfter(diff); 189 190 case "status": 191 case string.init: 192 return sendElapsedTime(diff); 193 194 default: 195 assert(0, "Unexpected inner case in nested onCommandStopwatch switch"); 196 } 197 198 case "clear": 199 if (event.sender.class_ < IRCUser.Class.operator) 200 { 201 return sendMissingClearPermissions(); 202 } 203 204 plugin.stopwatches.remove(event.channel); 205 return sendClearingStopwatches(event.channel); 206 207 default: 208 return sendUsage(); 209 } 210 } 211 212 213 mixin MinimalAuthentication; 214 mixin PluginRegistration!StopwatchPlugin; 215 216 public: 217 218 219 // StopwatchPlugin 220 /++ 221 The Stopwatch plugin offers the ability to start stopwatches, and print 222 how much time elapsed upon stopping them. 223 +/ 224 final class StopwatchPlugin : IRCPlugin 225 { 226 private: 227 /// All Stopwatch plugin settings. 228 StopwatchSettings stopwatchSettings; 229 230 /// Vote start timestamps by user by channel. 231 long[string][string] stopwatches; 232 233 mixin IRCPluginImpl; 234 }