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 }