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 }