Source file Plugin_vote.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
open Result
open Lwt_infix
module Time = struct
type t = float
let minutes x : t = float_of_int x *. 60.
let as_mins x = int_of_float @@ (x /. 60.)
let display_mins x =
match as_mins x with
| 0 -> "less than 1 minute"
| 1 -> "one minute"
| n -> Printf.sprintf "%d minutes" n
let now = Unix.gettimeofday
end
module Vote = struct
type vote = For | Against
type t = {
purpose: string;
expire: float;
status: (string, vote) Hashtbl.t;
mutable quorum: int option;
}
let start ?quorum ?(duration = Time.minutes 30) purpose =
{
purpose;
status = Hashtbl.create 10;
expire = Time.(now () +. duration);
quorum;
}
let add_vote t nick vote =
match CCHashtbl.get t.status nick with
| None -> Hashtbl.add t.status nick vote
| Some old_vote when old_vote = vote -> ()
| _ -> Hashtbl.replace t.status nick vote
type result = { for_: int; against: int }
let count_votes t : result =
Hashtbl.fold
(fun _ vote r ->
match vote with
| For -> { r with for_ = r.for_ + 1 }
| Against -> { r with against = r.against + 1 })
t.status { for_ = 0; against = 0 }
let vote_status t nick =
try Some (Hashtbl.find t.status nick) with Not_found -> None
let show_status t =
let r = count_votes t in
Printf.sprintf "expressed %d / for %d / against %d (expires in %s)"
(Hashtbl.length t.status) r.for_ r.against
Time.(display_mins @@ (t.expire -. now ()))
let missing_votes t : int option =
match t.quorum with
| None -> None
| Some n -> Some (max 0 (n - Hashtbl.length t.status))
let expired now { expire; _ } = expire < now
let is_complete t =
(match missing_votes t with
| Some 0 -> true
| _ -> false)
|| expired (Time.now ()) t
let get_winner t =
let r = count_votes t in
if r.for_ > r.against then
Some For
else if r.against < r.for_ then
Some Against
else
None
let string_of_vote = function
| For -> "for"
| Against -> "against"
let vote_of_string = function
| "for" -> Ok For
| "against" -> Ok Against
| _ -> Error "wrong vote (expected 'for' or 'against')"
end
type poll = { purpose: string; creator: string; vote: Vote.t }
type state = { polls: (string, poll) Hashtbl.t; mutable stop: bool }
let max_polls_per_nick = 1
let max_polls = 5
let nb_polls_per_nick polls nick =
Hashtbl.fold
(fun _ { creator; _ } count ->
if creator = nick then
count + 1
else
count)
polls 0
let show_status name { creator; vote; _ } =
Printf.sprintf "Poll %s (created by %s) : %s" name creator
(Vote.show_status vote)
let create_poll polls nick name purpose =
match Hashtbl.length polls with
| cur_len when cur_len >= max_polls ->
Error
"cannot create a new poll: max number has been reached, please delete \
one before proceeding"
| _ ->
(match nb_polls_per_nick polls nick with
| cur_polls when cur_polls >= max_polls_per_nick ->
Error
(Printf.sprintf
"cannot create a new poll: max number by user has been reached: %d, \
please delete one before proceeding"
max_polls_per_nick)
| _ ->
(match CCHashtbl.get polls name with
| Some poll ->
Error
(Printf.sprintf "a poll already exists with this name: %s"
(show_status name poll))
| None ->
let poll =
{ purpose = name; creator = nick; vote = Vote.start purpose }
in
Hashtbl.add polls name poll;
Ok
(Some
(Printf.sprintf "Poll %s successfully created! %s" name
(show_status name poll)))))
let vote polls nick name vote =
match CCHashtbl.get polls name with
| None -> Error (Printf.sprintf "no poll called '%s'" name)
| Some poll ->
(match Vote.vote_of_string vote with
| Error _ as e -> e
| Ok vote ->
Vote.add_vote poll.vote nick vote;
(match Vote.is_complete poll.vote with
| true ->
Hashtbl.remove polls name;
Ok
(Some
(Printf.sprintf "Poll time has ended!: The final result is %s"
(Option.value ~default:"draw"
@@ Option.map Vote.string_of_vote
@@ Vote.get_winner poll.vote)))
| _ -> Ok (Some (Vote.show_status poll.vote))))
let show_vote polls name nick =
match CCHashtbl.get polls name with
| None -> Error (Printf.sprintf "no active poll named '%s'" name)
| Some poll ->
let vote =
Option.value ~default:"draw"
@@ Option.map Vote.string_of_vote
@@ Vote.vote_status poll.vote nick
in
Ok (Some (Printf.sprintf "%s is %s %s" nick vote name))
let vote_status polls name =
match CCHashtbl.get polls name with
| None -> Error (Printf.sprintf "no active poll named '%s'" name)
| Some poll -> Ok (Some (show_status name poll))
let rec collector (st : state) : _ Lwt.t =
let now = Time.now () in
Hashtbl.iter
(fun name { vote; _ } ->
if Vote.expired now vote then Hashtbl.remove st.polls name)
st.polls;
Lwt_unix.sleep (Time.minutes 1) >>= fun () -> collector st
let help =
"!vote show <poll> <nick> : display current vote of <nick> for <poll>\n\
!vote start <poll> <description (optional)> : create new poll\n\
!vote status <poll> : display current status of <poll>\n\
!vote for <poll> : vote for the given <poll>\n\
!vote against <poll>: vote against the given <poll>\n"
let reply (self : state) msg s : _ Lwt.t =
let message_usage =
"Please use `!vote for VOTE_NAME` or `!vote against VOTE_NAME` to vote; or \
start a new vote with `!vote start VOTE_NAME`. (run !help vote for the \
complete list of commands)"
in
let reply_res = function
| Error msg ->
let message = Printf.sprintf "%s: %s" Talk.(select Err) msg in
Lwt.return @@ Some message
| Ok x -> Lwt.return x
in
match Stringext.split ~max:3 (String.trim s) ~on:' ' with
| "show" :: name :: nick :: _ -> show_vote self.polls name nick |> reply_res
| "start" :: name :: purpose ->
create_poll self.polls msg.Core.nick name
(match purpose with
| [] -> ""
| purpose :: _ -> purpose)
|> reply_res
| "status" :: name :: _ -> vote_status self.polls name |> reply_res
| (("for" | "against") as v) :: name :: _ ->
vote self.polls msg.Core.nick name v |> reply_res
| [ (("show" | "start" | "status" | "for" | "against") as v) ] ->
Error
(Printf.sprintf
"this command is missing the vote name. Please specify one as in \
`vote %sVOTE_NAME"
v)
|> reply_res
| _ -> Error ("invalid command. " ^ message_usage) |> reply_res
let cmd_vote state : Command.t =
Command.make_simple
~descr:("vote system for yes/no questions\n" ^ help)
~cmd:"vote" ~prio:10 (reply state)
let of_json _ _ : (state, _) result =
let polls = { stop = false; polls = Hashtbl.create 10 } in
Lwt.async (fun () -> collector polls);
Ok polls
let plugin =
Plugin.stateful ~name:"vote"
~to_json:(fun _ -> None)
~of_json
~commands:(fun state -> [ cmd_vote state ])
~stop:(fun st ->
st.stop <- true;
Lwt.return ())
()