Fix compilation warnings from gcc 4.6.
[srvx.git] / src / mod-helpserv.c
1 /* mod-helpserv.c - Support Helper assistant service
2  * Copyright 2002-2003, 2006 srvx Development Team
3  *
4  * This file is part of srvx.
5  *
6  * srvx is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with srvx; if not, write to the Free Software Foundation,
18  * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
19  */
20
21 /* Wishlist for helpserv.c
22  * - Make HelpServ send unassigned request count to helpers as they join the
23  *   channel
24  * - FAQ responses
25  * - Get cmd_statsreport to sort by time and show requests closed
26  * - .. then make statsreport show weighted combination of time and requests closed :)
27  * - Something for users to use a subset of commands... like closing their
28  *   own request(s), viewing what they've sent so far, and finding their
29  *   helper's nick.
30  * - It would be nice to have some variable expansions for the custom
31  *   request open/close/etc messages. ${request/id}, etc
32  * - Allow manipulation of the persist types on a per-request basis... so
33  *   if it's normally set to close requests after a user quits, but there
34  *   is a long-term issue, then that single request can remain
35  * - Bot suspension
36  */
37
38 #include "conf.h"
39 #include "global.h"
40 #include "modcmd.h"
41 #include "nickserv.h"
42 #include "opserv.h"
43 #include "saxdb.h"
44 #include "timeq.h"
45
46 #define HELPSERV_CONF_NAME "modules/helpserv"
47 #define HELPSERV_HELPFILE_NAME "mod-helpserv.help"
48 const char *helpserv_module_deps[] = { NULL };
49
50 /* db entries */
51 #define KEY_BOTS "bots"
52 #define KEY_LAST_STATS_UPDATE "last_stats_update"
53 #define KEY_NICK "nick"
54 #define KEY_DB_BADCHANS "badchans"
55 #define KEY_HELP_CHANNEL "help_channel"
56 #define KEY_PAGE_DEST "page_dest"
57 #define KEY_CMDWORD "cmdword"
58 #define KEY_PERSIST_LENGTH "persist_length"
59 #define KEY_REGISTERED "registered"
60 #define KEY_REGISTRAR "registrar"
61 #define KEY_IDWRAP "id_wrap"
62 #define KEY_REQ_MAXLEN "req_maxlen"
63 #define KEY_LAST_REQUESTID "last_requestid"
64 #define KEY_HELPERS "users"
65 #define KEY_HELPER_LEVEL "level"
66 #define KEY_HELPER_HELPMODE "helpmode"
67 #define KEY_HELPER_WEEKSTART "weekstart"
68 #define KEY_HELPER_STATS "stats"
69 #define KEY_HELPER_STATS_TIME "time"
70 #define KEY_HELPER_STATS_PICKUP "picked_up"
71 #define KEY_HELPER_STATS_CLOSE "closed"
72 #define KEY_HELPER_STATS_REASSIGNFROM "reassign_from"
73 #define KEY_HELPER_STATS_REASSIGNTO "reassign_to"
74 #define KEY_REQUESTS "requests"
75 #define KEY_REQUEST_HELPER "helper"
76 #define KEY_REQUEST_ASSIGNED "assigned"
77 #define KEY_REQUEST_HANDLE "handle"
78 #define KEY_REQUEST_TEXT "text"
79 #define KEY_REQUEST_OPENED "opened"
80 #define KEY_REQUEST_NICK "nick"
81 #define KEY_REQUEST_USERHOST "userhost"
82 #define KEY_REQUEST_CLOSED "closed"
83 #define KEY_REQUEST_CLOSEREASON "closereason"
84 #define KEY_NOTIFICATION "notification"
85 #define KEY_PRIVMSG_ONLY "privmsg_only"
86 #define KEY_REQ_ON_JOIN "req_on_join"
87 #define KEY_AUTO_VOICE "auto_voice"
88 #define KEY_AUTO_DEVOICE "auto_devoice"
89 #define KEY_LAST_ACTIVE "last_active"
90
91 /* General */
92 #define HSFMT_TIME               "%a, %d %b %Y %H:%M:%S %Z"
93 static const struct message_entry msgtab[] = {
94     { "HSMSG_READHELP_SUCCESS", "Read HelpServ help database in %lu.%03lu seconds." },
95     { "HSMSG_INVALID_BOT", "This command requires a valid HelpServ bot name." },
96     { "HSMSG_ILLEGAL_CHANNEL", "$b%s$b is an illegal channel; cannot use it." },
97     { "HSMSG_INTERNAL_COMMAND", "$b%s$b appears to be an internal HelpServ command, sorry." },
98     { "HSMSG_NOT_IN_USERLIST", "%s lacks access to $b%s$b." },
99     { "HSMSG_LEVEL_TOO_LOW", "You lack access to this command." },
100     { "HSMSG_OPER_CMD", "This command can only be executed via $O." },
101     { "HSMSG_WTF_WHO_ARE_YOU", "$bUnable to find you on the %s userlist.$b This is a bug. Please report it." },
102     { "HSMSG_NO_USE_OPSERV", "This command cannot be used via $O. If you really need to use it, add yourself to the userlist." },
103     { "HSMSG_OPSERV_NEED_USER", "To use this command via $O, you must supply a user to target." },
104     { "HSMSG_PAGE_REQUEST", "Page from $b%s$b: $b%s$b" },
105     { "HSMSG_BAD_REQ_TYPE", "I don't know how to list $b%s$b requests." },
106
107 /* Greetings */
108     { "HSMSG_GREET_EXISTING_REQ", "Welcome back to %s. You have already opened request ID#%lu. Any messages you send to $S will be appended to that request." },
109     { "HSMSG_GREET_PRIVMSG_EXISTREQ", "Hello again. Your account has a previously opened request ID#%lu. This message and any further messages you send to $S will be appended to that request." },
110
111 /* User Management */
112     { "HSMSG_CANNOT_ADD", "You do not have permission to add users." },
113     { "HSMSG_CANNOT_DEL", "You do not have permission to delete users." },
114     { "HSMSG_CANNOT_CLVL", "You do not have permission to modify users' access." },
115     { "HSMSG_NO_SELF_CLVL", "You cannot change your own access." },
116     { "HSMSG_NO_BUMP_ACCESS", "You cannot give users the same or more access than yourself." },
117     { "HSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
118     { "HSMSG_ADDED_USER", "Added new $b%s$b %s to the user list." },
119     { "HSMSG_DELETED_USER", "Deleted $b%s$b %s from the user list." },
120     { "HSMSG_USER_EXISTS", "$b%s$b is already on the user list." },
121     { "HSMSG_INVALID_ACCESS", "$b%s$b is an invalid access level." },
122     { "HSMSG_CHANGED_ACCESS", "%s now has $b%s$b access." },
123     { "HSMSG_EXPIRATION_DONE", "%d eligible HelpServ bots have retired." },
124     { "HSMSG_BAD_WEEKDAY", "I do not know which day of the week $b%s$b is." },
125     { "HSMSG_WEEK_STARTS", "$b%s$b's weeks start on $b%s$b." },
126     { "HSMSG_MODSTATS_BAD_FIELD", "The specified field does not exist." },
127     { "HSMSG_MODSTATS_BAD_WEEK", "The specified week is invalid." },
128     { "HSMSG_MODSTATS_NEGATIVE", "This modification would result in a negative value." },
129     { "HSMSG_MODSTATS_SUCCESS", "$b%s$b's stats have been modified successfully." },
130
131 /* Registration */
132     { "HSMSG_ILLEGAL_NICK", "$b%s$b is an illegal nick; cannot use it." },
133     { "HSMSG_NICK_EXISTS", "The nick %s is in use by someone else." },
134     { "HSMSG_REG_SUCCESS", "%s now has ownership of bot %s." },
135     { "HSMSG_NEED_UNREG_CONFIRM", "To unregister this bot, you must /msg $S unregister CONFIRM" },
136     { "HSMSG_ERROR_ADDING_SERVICE", "Error creating new user $b%s$b." },
137
138 /* Rename */
139     { "HSMSG_RENAMED", "%s has been renamed to $b%s$b." },
140     { "HSMSG_MOVE_SAME_CHANNEL", "You cannot move %s to the same channel it is on." },
141     { "HSMSG_INVALID_MOVE", "$b%s$b is not a valid nick or channel name." },
142     { "HSMSG_NEED_GIVEOWNERSHIP_CONFIRM", "To transfer ownership of this bot, you must /msg $S giveownership newowner CONFIRM" },
143     { "HSMSG_MULTIPLE_OWNERS", "There is more than one owner of %s; please use other commands to change ownership." },
144     { "HSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
145     { "HSMSG_OWNERSHIP_GIVEN", "Ownership of $b%s$b has been transferred to account $b%s$b." },
146
147 /* Queue settings */
148     { "HSMSG_INVALID_OPTION", "$b%s$b is not a valid option." },
149     { "HSMSG_QUEUE_OPTIONS", "HelpServ Queue Options:" },
150     { "HSMSG_SET_COMMAND_TYPE",   "$bPageType        $b %s" },
151     { "HSMSG_SET_ALERT_TYPE",     "$bAlertPageType   $b %s" },
152     { "HSMSG_SET_STATUS_TYPE",    "$bStatusPageType  $b %s" },
153     { "HSMSG_SET_COMMAND_TARGET", "$bPageTarget      $b %s" },
154     { "HSMSG_SET_ALERT_TARGET",   "$bAlertPageTarget $b %s" },
155     { "HSMSG_SET_STATUS_TARGET",  "$bStatusPageTarget$b %s" },
156     { "HSMSG_SET_GREETING",       "$bGreeting        $b %s" },
157     { "HSMSG_SET_REQOPENED",      "$bReqOpened       $b %s" },
158     { "HSMSG_SET_REQASSIGNED",    "$bReqAssigned     $b %s" },
159     { "HSMSG_SET_REQCLOSED",      "$bReqClosed       $b %s" },
160     { "HSMSG_SET_IDLEDELAY",      "$bIdleDelay       $b %s" },
161     { "HSMSG_SET_WHINEDELAY",     "$bWhineDelay      $b %s" },
162     { "HSMSG_SET_WHINEINTERVAL",  "$bWhineInterval   $b %s" },
163     { "HSMSG_SET_EMPTYINTERVAL",  "$bEmptyInterval   $b %s" },
164     { "HSMSG_SET_STALEDELAY",     "$bStaleDelay      $b %s" },
165     { "HSMSG_SET_REQPERSIST",     "$bReqPersist      $b %s" },
166     { "HSMSG_SET_HELPERPERSIST",  "$bHelperPersist   $b %s" },
167     { "HSMSG_SET_NOTIFICATION",   "$bNotification    $b %s" },
168     { "HSMSG_SET_IDWRAP",         "$bIDWrap          $b %d" },
169     { "HSMSG_SET_REQMAXLEN",      "$bReqMaxLen       $b %d" },
170     { "HSMSG_SET_PRIVMSGONLY",    "$bPrivmsgOnly     $b %s" },
171     { "HSMSG_SET_REQONJOIN",      "$bReqOnJoin       $b %s" },
172     { "HSMSG_SET_AUTOVOICE",      "$bAutoVoice       $b %s" },
173     { "HSMSG_SET_AUTODEVOICE",    "$bAutoDevoice     $b %s" },
174     { "HSMSG_PAGE_NOTICE", "notice" },
175     { "HSMSG_PAGE_PRIVMSG", "privmsg" },
176     { "HSMSG_PAGE_ONOTICE", "onotice" },
177     { "HSMSG_LENGTH_PART", "part" },
178     { "HSMSG_LENGTH_QUIT", "quit" },
179     { "HSMSG_LENGTH_CLOSE", "close" },
180     { "HSMSG_NOTIFY_DROP", "ReqDrop" },
181     { "HSMSG_NOTIFY_USERCHANGES", "UserChanges" },
182     { "HSMSG_NOTIFY_ACCOUNTCHANGES", "AccountChanges" },
183     { "HSMSG_INVALID_INTERVAL", "Sorry, %s must be at least %s." },
184     { "HSMSG_0_DISABLED", "0 (Disabled)" },
185     { "HSMSG_NEED_MANAGER", "Only managers or higher can do this." },
186     { "HSMSG_SET_NEED_OPER", "This option can only be set by an oper." },
187
188 /* Requests */
189     { "HSMSG_REQ_INVALID", "$b%s$b is not a valid request ID, or there are no requests for that nick or account." },
190     { "HSMSG_REQ_NOT_YOURS_ASSIGNED_TO", "Request $b%lu$b is not assigned to you; it is currently assigned to %s." },
191     { "HSMSG_REQ_NOT_YOURS_UNASSIGNED", "Request $b%lu$b is not assigned to you; it is currently unassigned." },
192     { "HSMSG_REQ_FOUNDMANY", "The user you entered had multiple requests. The oldest one is being used." },
193     { "HSMSG_REQ_CLOSED", "Request $b%lu$b has been closed." },
194     { "HSMSG_REQ_NO_UNASSIGNED", "There are no unassigned requests." },
195     { "HSMSG_USERCMD_NO_REQUEST", "You must have an open request to use a user command." },
196     { "HSMSG_USERCMD_UNKNOWN", "I do not know the user command $b%s$b." },
197     { "HSMSG_REQ_YOU_NOT_IN_HELPCHAN_OPEN", "You cannot open this request as you are not in %s." },
198     { "HSMSG_REQ_YOU_NOT_IN_HELPCHAN", "You cannot be assigned this request as you are not in %s." },
199     { "HSMSG_REQ_HIM_NOT_IN_HELPCHAN", "%s cannot be assigned this request as they are not in %s." },
200     { "HSMSG_REQ_SELF_NOT_IN_OVERRIDE", "You are being assigned this request even though you are not in %s, because the restriction was overridden (you are a manager or owner). If you join and then part, this request will be marked unassigned." },
201     { "HSMSG_REQ_YOU_NOT_IN_OVERRIDE", "Note: You are being assigned this request even though you are not in %s, because the restriction was overridden by a manager or owner. If you join and then part, this request will be marked unassigned." },
202     { "HSMSG_REQ_HIM_NOT_IN_OVERRIDE", "Note: %s is being assigned this request even though they are not in %s, because the restriction was overridden (you are a manager or owner). If they join and then part, this request will be marked unassigned." },
203     { "HSMSG_REQ_ASSIGNED_YOU", "You have been assigned request ID#%lu:" },
204     { "HSMSG_REQ_REASSIGNED", "You have been assigned request ID#%lu (reassigned from %s):" },
205     { "HSMSG_REQ_INFO_1", "Request ID#%lu:" },
206     { "HSMSG_REQ_INFO_2a", " - Nick %s / Account %s" },
207     { "HSMSG_REQ_INFO_2b", " - Nick %s / Not authed" },
208     { "HSMSG_REQ_INFO_2c", " - Online somewhere / Account %s" },
209     { "HSMSG_REQ_INFO_2d", " - Not online / Account %s" },
210     { "HSMSG_REQ_INFO_2e", " - Not online / No account" },
211     { "HSMSG_REQ_INFO_3", " - Opened at %s (%s ago)" },
212     { "HSMSG_REQ_INFO_4", " - Message:" },
213     { "HSMSG_REQ_INFO_MESSAGE", "   %s" },
214     { "HSMSG_REQ_ASSIGNED", "Your helper for request ID#%lu is %s (Current nick: %s)" },
215     { "HSMSG_REQ_ASSIGNED_AGAIN", "Your helper for request ID#%lu has been changed to %s (Current nick: %s)" },
216     { "HSMSG_REQ_UNASSIGNED", "Your helper for request ID#%lu has %s, so your request is no longer being handled by them. It has been placed near the front of the unhandled request queue, based on how long ago your request was opened." },
217     { "HSMSG_REQ_NEW", "Your message has been recorded and assigned request ID#%lu. A helper should contact you shortly." },
218     { "HSMSG_REQ_NEWONJOIN", "Welcome to %s. You have been assigned request ID#%lu. A helper should contact you shortly." },
219     { "HSMSG_REQ_UNHANDLED_TIME", "The oldest unhandled request has been waiting for %s." },
220     { "HSMSG_REQ_NO_UNHANDLED", "There are no other unhandled requests." },
221     { "HSMSG_REQ_PERSIST_QUIT", "Everything you tell me until you are helped (or you quit) will be recorded. If you disconnect, your request will be lost." },
222     { "HSMSG_REQ_PERSIST_PART", "Everything you tell me until you are helped (or you leave %s) will be recorded. If you part %s, your request will be lost." },
223     { "HSMSG_REQ_PERSIST_HANDLE", "Everything you tell me until you are helped will be recorded." },
224     { "HSMSG_REQ_MAXLEN", "Sorry, but your request has reached the maximum number of lines. Please wait to be assigned to a helper and continue explaining your request to them." },
225     { "HSMSQ_REQ_TEXT_ADDED", "Message from $b%s:$b %s" },
226     { "HSMSG_REQ_FOUND_ANOTHER", "Request ID#%lu has been closed. $S detected that you also have request ID#%lu open. If you send $S a message, it will be associated with that request." },
227
228 /* Messages that are inserted into request text */
229     { "HSMSG_REQMSG_NOTE_ADDED", "Your note for request ID#%lu has been recorded." },
230
231 /* Automatically generated page messages */
232     { "HSMSG_PAGE_NEW_REQUEST_AUTHED", "New request (ID#%lu) from $b%s$b (Account %s)" },
233     { "HSMSG_PAGE_NEW_REQUEST_UNAUTHED", "New request (ID#%lu) from $b%s$b (Not logged in)" },
234     { "HSMSG_PAGE_UPD_REQUEST_AUTHED", "Request ID#%lu has been updated by $b%s$b (Account %s). Request was initially opened at %s, and was last updated %s ago." },
235     { "HSMSG_PAGE_UPD_REQUEST_NOT_AUTHED", "Request ID#%lu has been updated by $b%s$b (not authed). Request was initially opened at %s, and was last updated %s ago." },
236     { "HSMSG_PAGE_CLOSE_REQUEST_1", "Request ID#%lu from $b%s$b (Account %s) has been closed by %s." },
237     { "HSMSG_PAGE_CLOSE_REQUEST_2", "Request ID#%lu from $b%s$b (Not authed) has been closed by %s." },
238     { "HSMSG_PAGE_CLOSE_REQUEST_3", "Request ID#%lu from an offline user (Account %s) has been closed by %s." },
239     { "HSMSG_PAGE_CLOSE_REQUEST_4", "Request ID#%lu from an offline user (no account) has been closed by %s." },
240     { "HSMSG_PAGE_ASSIGN_REQUEST_1", "Request ID#%lu from $b%s$b (Account %s) has been assigned to %s." },
241     { "HSMSG_PAGE_ASSIGN_REQUEST_2", "Request ID#%lu from $b%s$b (Not authed) has been assigned to %s." },
242     { "HSMSG_PAGE_ASSIGN_REQUEST_3", "Request ID#%lu from an offline user (Account %s) has been assigned to %s." },
243     { "HSMSG_PAGE_ASSIGN_REQUEST_4", "Request ID#%lu from an offline user (no account) has been assigned to %s." },
244     /* The last %s is still an I18N lose.  Blame whoever decided to overload it so much. */
245     { "HSMSG_PAGE_HELPER_GONE_1", "Request ID#%lu from $b%s$b (Account %s) $bhas been unassigned$b, as its helper, %s has %s." },
246     { "HSMSG_PAGE_HELPER_GONE_2", "Request ID#%lu from $b%s$b (Not authed) $bhas been unassigned$b, as its helper, %s has %s." },
247     { "HSMSG_PAGE_HELPER_GONE_3", "Request ID#%lu from an offline user (Account %s) $bhas been unassigned$b, as its helper, %s has %s." },
248     { "HSMSG_PAGE_HELPER_GONE_4", "Request ID#%lu from an offline user (No account) $bhas been unassigned$b, as its helper, %s has %s." },
249     { "HSMSG_PAGE_WHINE_HEADER", "$b%u unhandled request(s)$b waiting at least $b%s$b (%u unhandled, %u total)" },
250     { "HSMSG_PAGE_IDLE_HEADER", "$b%u users$b in %s $bidle at least %s$b:" },
251     { "HSMSG_PAGE_EMPTYALERT", "$b%s has no helpers present (%u unhandled request(s))$b" },
252     { "HSMSG_PAGE_ONLYTRIALALERT", "$b%s has no full helpers present (%d trial(s) present; %u unhandled request(s))$b" },
253     { "HSMSG_PAGE_FIRSTEMPTYALERT", "$b%s has no helpers present because %s has left (%u unhandled request(s))$b" },
254     { "HSMSG_PAGE_FIRSTONLYTRIALALERT", "$b%s has no full helpers present because %s has left (%d trial(s) present; %u unhandled request(s))$b" },
255     { "HSMSG_PAGE_EMPTYNOMORE", "%s has joined %s; cancelling the \"no helpers present\" alert" },
256
257 /* Notification messages */
258     { "HSMSG_NOTIFY_USER_QUIT", "The user for request ID#%lu, $b%s$b, has disconnected." },
259     { "HSMSG_NOTIFY_USER_MOVE", "The account for request ID#%lu, $b%s$b has been unregistered. It has been associated with user $b%s$b." },
260     { "HSMSG_NOTIFY_USER_NICK", "The user for request ID#%lu has changed their nick from $b%s$b to $b%s$b." },
261     { "HSMSG_NOTIFY_USER_FOUND", "The user for request ID#%lu is now online using nick $b%s$b." },
262     { "HSMSG_NOTIFY_HAND_RENAME", "The account for request ID#%lu has been renamed from $b%s$b to $b%s$b." },
263     { "HSMSG_NOTIFY_HAND_MOVE", "The account for request ID#%lu has been changed to $b%s$b from $b%s$b." },
264     { "HSMSG_NOTIFY_HAND_STUCK", "The user for request ID#%lu, $b%s$b, has re-authenticated to account $b%s$b from $b%s$b, and the request remained associated with the old handle." },
265     { "HSMSG_NOTIFY_HAND_AUTH", "The user for request ID#%lu, $b%s$b, has authenticated to account $b%s$b." },
266     { "HSMSG_NOTIFY_HAND_UNREG", "The account for request ID#%lu, $b%s$b, has been unregistered by $b%s$b." },
267     { "HSMSG_NOTIFY_HAND_MERGE", "The account for request ID#%lu, $b%s$b, has been merged with $b%s$b by %s." },
268     { "HSMSG_NOTIFY_ALLOWAUTH", "The user for request ID#%lu, $b%s$b, has been permitted by %s to authenticate to account $b%s$b without hostmask checking." },
269     { "HSMSG_NOTIFY_UNALLOWAUTH", "The user for request ID#%lu, $b%s$b, has had their ability to authenticate without hostmask checking revoked by %s." },
270     { "HSMSG_NOTIFY_FAILPW", "The user for request ID#%lu, $b%s$b, has attempted to authenticate to account $b%s$b, but used an incorrect password." },
271     { "HSMSG_NOTIFY_REQ_DROP_PART", "Request ID#%lu has been $bdropped$b because %s left the help channel." },
272     { "HSMSG_NOTIFY_REQ_DROP_QUIT", "Request ID#%lu has been $bdropped$b because %s quit IRC." },
273     { "HSMSG_NOTIFY_REQ_DROP_UNREGGED", "Request ID#%lu (account %s) has been $bdropped$b because the account was unregistered." },
274
275 /* Presence and request-related messages */
276     { "HSMSG_REQ_DROPPED_PART", "You have left $b%s$b. Your help request (ID#%lu) has been deleted." },
277     { "HSMSG_REQ_WARN_UNREG", "The account you were authenticated to ($b%s$b) has been unregistered. Therefore unless you register a new handle, or authenticate to another one, if you disconnect, your HelpServ $S (%s) request ID#%lu will be lost." },
278     { "HSMSG_REQ_DROPPED_UNREG", "By unregistering the account $b%s$b, HelpServ $S (%s) request ID#%lu was dropped, as there was nobody online to associate the request with." },
279     { "HSMSG_REQ_ASSIGNED_UNREG", "As the account $b%s$b was unregistered, HelpServ $S (%s) request ID#%lu was associated with you, as you were authenticated to that account. If you disconnect, then this request will be lost forever." },
280     { "HSMSG_REQ_AUTH_STUCK", "By authenticating to account $b%s$b, HelpServ $S (%s) request ID#%lu remained with the previous account $b%s$b (because the request will remain until closed). This means that if you send $S a message, it $uwill not$u be associated with this request." },
281     { "HSMSG_REQ_AUTH_MOVED", "By authenticating to account $b%s$b, HelpServ $S (%s) request ID#%lu ceased to be associated with your previously authenticated account ($b%s$b), and was transferred to your new account (because the request will only remain until you %s). This means that if you send $S a message, it $uwill$u be associated with this request." },
282
283 /* Lists */
284     { "HSMSG_BOTLIST_HEADER", "$bCurrent HelpServ bots:$b" },
285     { "HSMSG_USERLIST_HEADER", "$b%s users:$b" },
286     { "HSMSG_USERLIST_ZOOT_LVL", "%s $b%ss$b:" },
287     { "HSMSG_REQLIST_AUTH", "You are currently assigned these requests:" },
288     { "HSMSG_REQ_LIST_TOP_UNASSIGNED", "Listing $ball unassigned$b requests (%d in list)." },
289     { "HSMSG_REQ_LIST_TOP_ASSIGNED", "Listing $ball assigned$b requests (%d in list)." },
290     { "HSMSG_REQ_LIST_TOP_YOUR", "Listing $byour$b requests (%d in list)." },
291     { "HSMSG_REQ_LIST_TOP_ALL", "Listing $ball$b requests (%d in list)." },
292     { "HSMSG_REQ_LIST_NONE", "There are no matching requests." },
293     { "HSMSG_STATS_TOP", "Stats for %s user $b%s$b (week starts %s):" },
294     { "HSMSG_STATS_TIME", "$uTime spent helping in %s:$u" },
295     { "HSMSG_STATS_REQS", "$uRequest activity statistics:$u" },
296
297 /* Status report headers */
298     { "HSMSG_STATS_REPORT_0", "Stats report for current week" },
299     { "HSMSG_STATS_REPORT_1", "Stats report for one week ago" },
300     { "HSMSG_STATS_REPORT_2", "Stats report for two weeks ago" },
301     { "HSMSG_STATS_REPORT_3", "Stats report for three weeks ago" },
302
303 /* Responses to user commands */
304     { "HSMSG_YOU_BEING_HELPED", "You are already being helped." },
305     { "HSMSG_YOU_BEING_HELPED_BY", "You are already being helped by $b%s$b." },
306     { "HSMSG_WAIT_STATUS", "You are %d of %d in line; the first person has waited %s." },
307     { NULL, NULL }
308 };
309
310 enum helpserv_level {
311     HlNone,
312     HlTrial,
313     HlHelper,
314     HlManager,
315     HlOwner,
316     HlOper,
317     HlCount
318 };
319
320 static const char *helpserv_level_names[] = {
321     "None",
322     "Trial",
323     "Helper",
324     "Manager",
325     "Owner",
326     "Oper",
327     NULL
328 };
329
330 enum page_type {
331     PAGE_NONE,
332     PAGE_NOTICE,
333     PAGE_PRIVMSG,
334     PAGE_ONOTICE,
335     PAGE_COUNT
336 };
337
338 static const struct {
339     char *db_name;
340     char *print_name;
341     irc_send_func func;
342 } page_types[] = {
343     { "none", "MSG_NONE", NULL },
344     { "notice", "HSMSG_PAGE_NOTICE", irc_notice },
345     { "privmsg", "HSMSG_PAGE_PRIVMSG", irc_privmsg },
346     { "onotice", "HSMSG_PAGE_ONOTICE", irc_wallchops },
347     { NULL, NULL, NULL }
348 };
349
350 enum page_source {
351     PGSRC_COMMAND,
352     PGSRC_ALERT,
353     PGSRC_STATUS,
354     PGSRC_COUNT
355 };
356
357 static const struct {
358     char *db_name;
359     char *print_target;
360     char *print_type;
361 } page_sources[] = {
362     { "command", "HSMSG_SET_COMMAND_TARGET", "HSMSG_SET_COMMAND_TYPE" },
363     { "alert", "HSMSG_SET_ALERT_TARGET", "HSMSG_SET_ALERT_TYPE" },
364     { "status", "HSMSG_SET_STATUS_TARGET", "HSMSG_SET_STATUS_TYPE" },
365     { NULL, NULL, NULL }
366 };
367
368 enum message_type {
369     MSGTYPE_GREETING,
370     MSGTYPE_REQ_OPENED,
371     MSGTYPE_REQ_ASSIGNED,
372     MSGTYPE_REQ_CLOSED,
373     MSGTYPE_REQ_DROPPED,
374     MSGTYPE_COUNT
375 };
376
377 static const struct {
378     char *db_name;
379     char *print_name;
380 } message_types[] = {
381     { "greeting", "HSMSG_SET_GREETING" },
382     { "reqopened", "HSMSG_SET_REQOPENED" },
383     { "reqassigned", "HSMSG_SET_REQASSIGNED" },
384     { "reqclosed", "HSMSG_SET_REQCLOSED" },
385     { "reqdropped", "HSMSG_SET_REQDROPPED" },
386     { NULL, NULL }
387 };
388
389 enum interval_type {
390     INTERVAL_IDLE_DELAY,
391     INTERVAL_WHINE_DELAY,
392     INTERVAL_WHINE_INTERVAL,
393     INTERVAL_EMPTY_INTERVAL,
394     INTERVAL_STALE_DELAY,
395     INTERVAL_COUNT
396 };
397
398 static const struct {
399     char *db_name;
400     char *print_name;
401 } interval_types[] = {
402     { "idledelay", "HSMSG_SET_IDLEDELAY" },
403     { "whinedelay", "HSMSG_SET_WHINEDELAY" },
404     { "whineinterval", "HSMSG_SET_WHINEINTERVAL" },
405     { "emptyinterval", "HSMSG_SET_EMPTYINTERVAL" },
406     { "staledelay", "HSMSG_SET_STALEDELAY" },
407     { NULL, NULL }
408 };
409
410 enum persistence_type {
411     PERSIST_T_REQUEST,
412     PERSIST_T_HELPER,
413     PERSIST_T_COUNT
414 };
415
416 static const struct {
417     char *db_name;
418     char *print_name;
419 } persistence_types[] = {
420     { "reqpersist", "HSMSG_SET_REQPERSIST" },
421     { "helperpersist", "HSMSG_SET_HELPERPERSIST" },
422     { NULL, NULL }
423 };
424
425 enum persistence_length {
426     PERSIST_PART,
427     PERSIST_QUIT,
428     PERSIST_CLOSE,
429     PERSIST_COUNT
430 };
431
432 static const struct {
433     char *db_name;
434     char *print_name;
435 } persistence_lengths[] = {
436     { "part", "HSMSG_LENGTH_PART" },
437     { "quit", "HSMSG_LENGTH_QUIT" },
438     { "close", "HSMSG_LENGTH_CLOSE" },
439     { NULL, NULL }
440 };
441
442 enum notification_type {
443     NOTIFY_NONE,
444     NOTIFY_DROP,
445     NOTIFY_USER,
446     NOTIFY_HANDLE,
447     NOTIFY_COUNT
448 };
449
450 static const struct {
451     char *db_name;
452     char *print_name;
453 } notification_types[] = {
454     { "none", "MSG_NONE" },
455     { "reqdrop", "HSMSG_NOTIFY_DROP" },
456     { "userchanges", "HSMSG_NOTIFY_USERCHANGES" },
457     { "accountchanges", "HSMSG_NOTIFY_ACCOUNTCHANGES" },
458     { NULL, NULL }
459 };
460
461 static const char *weekday_names[] = {
462     "Sunday",
463     "Monday",
464     "Tuesday",
465     "Wednesday",
466     "Thursday",
467     "Friday",
468     "Saturday",
469     NULL
470 };
471
472 static const char *statsreport_week[] = {
473     "HSMSG_STATS_REPORT_0",
474     "HSMSG_STATS_REPORT_1",
475     "HSMSG_STATS_REPORT_2",
476     "HSMSG_STATS_REPORT_3"
477 };
478
479 static struct {
480     const char *description;
481     const char *reqlogfile;
482     unsigned long db_backup_frequency;
483     unsigned int expire_age;
484     unsigned long modstats_level;
485     char user_escape;
486 } helpserv_conf;
487
488 static unsigned long last_stats_update;
489 static int shutting_down;
490 static FILE *reqlog_f;
491 static struct log_type *HS_LOG;
492
493 #define CMD_NEED_BOT            0x001
494 #define CMD_NOT_OVERRIDE        0x002
495 #define CMD_FROM_OPSERV_ONLY    0x004
496 #define CMD_IGNORE_EVENT        0x008
497 #define CMD_NEVER_FROM_OPSERV   0x010
498
499 struct helpserv_bot {
500     struct userNode *helpserv;
501
502     struct chanNode *helpchan;
503
504     struct chanNode *page_targets[PGSRC_COUNT];
505     enum page_type page_types[PGSRC_COUNT];
506     char *messages[MSGTYPE_COUNT];
507     unsigned long intervals[INTERVAL_COUNT];
508     enum notification_type notify;
509
510     /* This is a default; it can be changed on a per-request basis */
511     enum persistence_length persist_lengths[PERSIST_T_COUNT];
512
513     dict_t users; /* indexed by handle */
514
515     struct helpserv_request *unhandled; /* linked list of unhandled requests */
516     dict_t requests; /* indexed by request id */
517     unsigned long last_requestid;
518     unsigned long id_wrap;
519     unsigned long req_maxlen; /* Maxmimum request length in lines */
520
521     unsigned int privmsg_only : 1;
522     unsigned int req_on_join : 1;
523     unsigned int auto_voice : 1;
524     unsigned int auto_devoice : 1;
525
526     unsigned int helpchan_empty : 1;
527
528     unsigned long registered;
529     unsigned long last_active;
530     char *registrar;
531 };
532
533 struct helpserv_user {
534     struct handle_info *handle;
535     struct helpserv_bot *hs;
536     unsigned int help_mode : 1;
537     unsigned int week_start : 3;
538     enum helpserv_level level;
539     /* statistics */
540     unsigned long join_time; /* when they joined, or 0 if not in channel */
541     /* [0] through [3] are n weeks ago, [4] is the total of everything before that */
542     unsigned int time_per_week[5]; /* how long they've were in the channel the past 4 weeks */
543     unsigned int picked_up[5]; /* how many requests they have picked up */
544     unsigned int closed[5]; /* how many requests they have closed */
545     unsigned int reassigned_from[5]; /* how many requests reassigned from them to others */
546     unsigned int reassigned_to[5]; /* how many requests reassigned from others to them */
547 };
548
549 struct helpserv_request {
550     struct helpserv_user *helper;
551     struct helpserv_bot *hs;
552     struct string_list *text;
553     struct helpserv_request *next_unhandled;
554
555     struct helpserv_reqlist *parent_nick_list;
556     struct helpserv_reqlist *parent_hand_list;
557
558     /* One, but not both, of "user" and "handle" may be NULL,
559      * depending on what we know about the user.
560      *
561      * If persist == PERSIST_CLOSE when the user quits, then it
562      * switches to handle instead of user... and stays that way (it's
563      * possible to have >1 nick per handle, so you can't really decide
564      * to reassign a userNode to it unless they send another message
565      * to HelpServ).
566      */
567     struct userNode *user;
568     struct handle_info *handle;
569
570     unsigned long id;
571     unsigned long opened;
572     unsigned long assigned;
573     unsigned long updated;
574 };
575
576 #define DEFINE_LIST_ALLOC(STRUCTNAME) \
577 struct STRUCTNAME * STRUCTNAME##_alloc() {\
578     struct STRUCTNAME *newlist; \
579     newlist = malloc(sizeof(struct STRUCTNAME)); \
580     STRUCTNAME##_init(newlist); \
581     return newlist; \
582 }\
583 void STRUCTNAME##_free(void *data) {\
584     struct STRUCTNAME *list = data; /* void * to let dict_set_free_data use it */ \
585     STRUCTNAME##_clean(list); \
586     free(list); \
587 }
588
589 DECLARE_LIST(helpserv_botlist, struct helpserv_bot *);
590 DEFINE_LIST(helpserv_botlist, struct helpserv_bot *)
591 DEFINE_LIST_ALLOC(helpserv_botlist)
592
593 DECLARE_LIST(helpserv_reqlist, struct helpserv_request *);
594 DEFINE_LIST(helpserv_reqlist, struct helpserv_request *)
595 DEFINE_LIST_ALLOC(helpserv_reqlist)
596
597 DECLARE_LIST(helpserv_userlist, struct helpserv_user *);
598 DEFINE_LIST(helpserv_userlist, struct helpserv_user *)
599 DEFINE_LIST_ALLOC(helpserv_userlist)
600
601 struct helpfile *helpserv_helpfile;
602 static struct module *helpserv_module;
603 static dict_t helpserv_func_dict;
604 static dict_t helpserv_usercmd_dict; /* contains helpserv_usercmd_t */
605 static dict_t helpserv_option_dict;
606 static dict_t helpserv_bots_dict; /* indexed by nick */
607 static dict_t helpserv_bots_bychan_dict; /* indexed by chan, holds a struct helpserv_botlist */
608 /* QUESTION: why are these outside of any helpserv_bot struct? */
609 static dict_t helpserv_reqs_bynick_dict; /* indexed by nick, holds a struct helpserv_reqlist */
610 static dict_t helpserv_reqs_byhand_dict; /* indexed by handle, holds a struct helpserv_reqlist */
611 static dict_t helpserv_users_byhand_dict; /* indexed by handle, holds a struct helpserv_userlist */
612
613 /* This is so that overrides can "speak" from opserv */
614 extern struct userNode *opserv;
615
616 #define HELPSERV_SYNTAX() helpserv_help(hs, from_opserv, user, argv[0])
617 #define HELPSERV_FUNC(NAME) int NAME(struct userNode *user, UNUSED_ARG(struct helpserv_bot *hs), int from_opserv, UNUSED_ARG(unsigned int argc), UNUSED_ARG(char *argv[]))
618 typedef HELPSERV_FUNC(helpserv_func_t);
619 #define HELPSERV_USERCMD(NAME) void NAME(struct helpserv_request *req, UNUSED_ARG(struct userNode *likely_helper), UNUSED_ARG(char *args))
620 typedef HELPSERV_USERCMD(helpserv_usercmd_t);
621 #define HELPSERV_OPTION(NAME) HELPSERV_FUNC(NAME)
622 typedef HELPSERV_OPTION(helpserv_option_func_t);
623
624 static HELPSERV_FUNC(cmd_help);
625
626 #define REQUIRE_PARMS(N) if (argc < N) { \
627         helpserv_notice(user, "MSG_MISSING_PARAMS", argv[0]); \
628         HELPSERV_SYNTAX(); \
629         return 0; }
630
631 /* For messages going to users being helped */
632 #if defined(GCC_VARMACROS)
633 # define helpserv_msguser(target, ARGS...) send_message_type((from_opserv ? 0 : hs->privmsg_only), (target), (from_opserv ? opserv : hs->helpserv), ARGS)
634 # define helpserv_user_reply(ARGS...) send_message_type(req->hs->privmsg_only, req->user, req->hs->helpserv, ARGS)
635 /* For messages going to helpers */
636 # define helpserv_notice(target, ARGS...) send_message((target), (from_opserv ? opserv : hs->helpserv), ARGS)
637 # define helpserv_notify(helper, ARGS...) do { struct userNode *_target; for (_target = (helper)->handle->users; _target; _target = _target->next_authed) { \
638         if(!_target->next_authed || GetUserMode(helper->hs->helpchan, _target)) {\
639           send_message(_target, (helper)->hs->helpserv, ARGS); \
640           break; \
641         } \
642     } } while (0)
643 # define helpserv_page(TYPE, ARGS...) do { \
644     int msg_type=0; struct chanNode *target=helpserv_get_page_type(hs, (TYPE), &msg_type); \
645     if (target) send_target_message(msg_type, target->name, hs->helpserv, ARGS); \
646     } while (0)
647 #elif defined(C99_VARMACROS)
648 # define helpserv_msguser(target, ...) send_message_type((from_opserv ? 0 : hs->privmsg_only), (target), (from_opserv ? opserv : hs->helpserv), __VA_ARGS__)
649 # define helpserv_user_reply(...) send_message_type(req->hs->privmsg_only, req->user, req->hs->helpserv, __VA_ARGS__)
650 /* For messages going to helpers */
651 # define helpserv_notice(target, ...) send_message((target), (from_opserv ? opserv : hs->helpserv), __VA_ARGS__)
652 # define helpserv_notify(helper, ...) do { struct userNode *_target; for (_target = (helper)->handle->users; _target; _target = _target->next_authed) { \
653         if(!_target->next_authed || GetUserMode(helper->hs->helpchan, _target)) {\
654           send_message(_target, (helper)->hs->helpserv, __VA_ARGS__); \
655           break; \
656         } \
657     } } while (0)
658 # define helpserv_page(TYPE, ...) do { \
659     int msg_type=0; struct chanNode *target=helpserv_get_page_type(hs, (TYPE), &msg_type); \
660     if (target) send_target_message(msg_type, target->name, hs->helpserv, __VA_ARGS__); \
661     } while (0)
662 #endif
663 #define helpserv_message(hs, target, id) do { if ((hs)->messages[id]) { \
664     if (from_opserv) \
665         send_message_type(4, (target), opserv, "%s", (hs)->messages[id]); \
666     else \
667         send_message_type(4 | hs->privmsg_only, (target), hs->helpserv, "%s", (hs)->messages[id]); \
668     } } while (0)
669 #define helpserv_get_handle_info(user, text) smart_get_handle_info((from_opserv ? opserv : hs->helpserv) , (user), (text))
670
671 struct helpserv_cmd {
672     enum helpserv_level access;
673     helpserv_func_t *func;
674     double weight;
675     long flags;
676 };
677
678 static void run_empty_interval(void *data);
679
680 static void helpserv_interval(char *output, unsigned long interval) {
681     int num_hours = interval / 3600;
682     int num_minutes = (interval % 3600) / 60;
683     sprintf(output, "%u hour%s, %u minute%s", num_hours, num_hours == 1 ? "" : "s", num_minutes, num_minutes == 1 ? "" : "s");
684 }
685
686 static const char * helpserv_level2str(enum helpserv_level level) {
687     if (level <= HlOper) {
688         return helpserv_level_names[level];
689     } else {
690         log_module(HS_LOG, LOG_ERROR, "helpserv_level2str receieved invalid level %d.", level);
691         return "Error";
692     }
693 }
694
695 static enum helpserv_level helpserv_str2level(const char *msg) {
696     enum helpserv_level nn;
697     for (nn=HlNone; nn<=HlOper; nn++) {
698         if (!irccasecmp(msg, helpserv_level_names[nn]))
699             return nn;
700     }
701     log_module(HS_LOG, LOG_ERROR, "helpserv_str2level received invalid level %s.", msg);
702     return HlNone; /* Error */
703 }
704
705 static struct helpserv_user *GetHSUser(struct helpserv_bot *hs, struct handle_info *hi) {
706     return dict_find(hs->users, hi->handle, NULL);
707 }
708
709 static void helpserv_log_request(struct helpserv_request *req, const char *reason) {
710     char key[27+NICKLEN];
711     char userhost[USERLEN+HOSTLEN+2];
712     struct saxdb_context *ctx;
713     int res;
714
715     assert(req != NULL);
716     assert(reason != NULL);
717     if (!reqlog_f || !(ctx = saxdb_open_context(reqlog_f)))
718         return;
719     sprintf(key, "%s-%lu-%lu", req->hs->helpserv->nick, (unsigned long)req->opened, req->id);
720     if ((res = setjmp(*saxdb_jmp_buf(ctx))) != 0) {
721         log_module(HS_LOG, LOG_ERROR, "Unable to log helpserv request: %s.", strerror(res));
722     } else {
723         saxdb_start_record(ctx, key, 1);
724         if (req->helper) {
725             saxdb_write_string(ctx, KEY_REQUEST_HELPER, req->helper->handle->handle);
726             saxdb_write_int(ctx, KEY_REQUEST_ASSIGNED, req->assigned);
727         }
728         if (req->handle) {
729             saxdb_write_string(ctx, KEY_REQUEST_HANDLE, req->handle->handle);
730         }
731         if (req->user) {
732             saxdb_write_string(ctx, KEY_REQUEST_NICK, req->user->nick);
733             sprintf(userhost, "%s@%s", req->user->ident, req->user->hostname);
734             saxdb_write_string(ctx, KEY_REQUEST_USERHOST, userhost);
735         }
736         saxdb_write_int(ctx, KEY_REQUEST_OPENED, req->opened);
737         saxdb_write_int(ctx, KEY_REQUEST_CLOSED, now);
738         saxdb_write_string(ctx, KEY_REQUEST_CLOSEREASON, reason);
739         saxdb_write_string_list(ctx, KEY_REQUEST_TEXT, req->text);
740         saxdb_end_record(ctx);
741         saxdb_close_context(ctx, 0);
742     }
743 }
744
745 static struct chanNode *helpserv_get_page_type(struct helpserv_bot *hs, enum page_source type, int *msg_type)
746 {
747     switch (hs->page_types[type]) {
748         case PAGE_NOTICE:
749             *msg_type = 0;
750             break;
751         case PAGE_PRIVMSG:
752             *msg_type = 1;
753             break;
754         case PAGE_ONOTICE:
755             *msg_type = 2;
756             break;
757         default:
758             log_module(HS_LOG, LOG_ERROR, "helpserv_page() called but %s has an invalid page type %d.", hs->helpserv->nick, type);
759             /* and fall through */
760         case PAGE_NONE:
761             return NULL;
762     }
763     return hs->page_targets[type];
764 }
765
766 /* Searches for a request by number, nick, or account (num|nick|*account).
767  * As there can potentially be >1 match, it takes a reqlist. The return
768  * value is the "best" request found (explained in the comment block below).
769  *
770  * If num_results is not NULL, it is set to the number of potentially matching
771  * requests.
772  * If hs_user is not NULL, requests assigned to this helper will be given
773  * preference (oldest assigned, falling back to oldest if there are none).
774  */
775 static struct helpserv_request * smart_get_request(struct helpserv_bot *hs, struct helpserv_user *hs_user, const char *needle, int *num_results) {
776     struct helpserv_reqlist *reqlist, resultlist;
777     struct helpserv_request *req, *oldest=NULL, *oldest_assigned=NULL;
778     struct userNode *user;
779     unsigned int i;
780
781     if (num_results)
782         *num_results = 0;
783
784     if (*needle == '*') {
785         /* This test (handle) requires the least processing, so it's first */
786         if (!(reqlist = dict_find(helpserv_reqs_byhand_dict, needle+1, NULL)))
787             return NULL;
788         helpserv_reqlist_init(&resultlist);
789         for (i=0; i < reqlist->used; i++) {
790             req = reqlist->list[i];
791             if (req->hs == hs) {
792                 helpserv_reqlist_append(&resultlist, req);
793                 if (num_results)
794                     (*num_results)++;
795             }
796         }
797     } else if (!needle[strspn(needle, "0123456789")]) {
798         /* The string is 100% numeric - a request id */
799         if (!(req = dict_find(hs->requests, needle, NULL)))
800             return NULL;
801         if (num_results)
802             *num_results = 1;
803         return req;
804     } else if ((user = GetUserH(needle))) {
805         /* And finally, search by nick */
806         if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, needle, NULL)))
807             return NULL;
808         helpserv_reqlist_init(&resultlist);
809
810         for (i=0; i < reqlist->used; i++) {
811             req = reqlist->list[i];
812             if (req->hs == hs) {
813                 helpserv_reqlist_append(&resultlist, req);
814                 if (num_results)
815                     (*num_results)++;
816             }
817         }
818         /* If the nick didn't have anything, try their handle */
819         if (!resultlist.used && user->handle_info) {
820             char star_handle[NICKSERV_HANDLE_LEN+2];
821
822             helpserv_reqlist_clean(&resultlist);
823             sprintf(star_handle, "*%s", user->handle_info->handle);
824
825             return smart_get_request(hs, hs_user, star_handle, num_results);
826         }
827     } else {
828         return NULL;
829     }
830
831     if (resultlist.used == 0) {
832         helpserv_reqlist_clean(&resultlist);
833         return NULL;
834     } else if (resultlist.used == 1) {
835         req = resultlist.list[0];
836         helpserv_reqlist_clean(&resultlist);
837         return req;
838     }
839
840     /* In case there is >1 request returned, use the oldest one assigned to
841      * the helper executing the command. Otherwise, use the oldest request.
842      * This may not be the intended result for cmd_pickup (first unhandled
843      * request may be better), or cmd_reassign (first handled request), but
844      * it's close enough, and there really aren't supposed to be multiple
845      * requests per person anyway; they're just side effects of authing. */
846
847     for (i=0; i < resultlist.used; i++) {
848         req = resultlist.list[i];
849         if (!oldest || req->opened < oldest->opened)
850             oldest = req;
851         if (hs_user && (!oldest_assigned || (req->helper == hs_user && req->opened < oldest_assigned->opened)))
852             oldest_assigned = req;
853     }
854
855     helpserv_reqlist_clean(&resultlist);
856
857     return oldest_assigned ? oldest_assigned : oldest;
858 }
859
860 static struct helpserv_request * create_request(struct userNode *user, struct helpserv_bot *hs, int from_join) {
861     struct helpserv_request *req = calloc(1, sizeof(struct helpserv_request));
862     char lbuf[3][MAX_LINE_SIZE], req_id[INTERVALLEN];
863     struct helpserv_reqlist *reqlist, *hand_reqlist;
864     const unsigned int from_opserv = 0;
865     const char *fmt;
866
867     assert(req);
868
869     req->id = ++hs->last_requestid;
870     sprintf(req_id, "%lu", req->id);
871     dict_insert(hs->requests, strdup(req_id), req);
872
873     if (hs->id_wrap) {
874         unsigned long i;
875         char buf[12];
876         if (hs->last_requestid < hs->id_wrap) {
877             for (i=hs->last_requestid; i < hs->id_wrap; i++) {
878                 sprintf(buf, "%lu", i);
879                 if (!dict_find(hs->requests, buf, NULL)) {
880                     hs->last_requestid = i-1;
881                     break;
882                 }
883             }
884         }
885         if (hs->last_requestid >= hs->id_wrap) {
886             for (i=1; i < hs->id_wrap; i++) {
887                 sprintf(buf, "%lu", i);
888                 if (!dict_find(hs->requests, buf, NULL)) {
889                     hs->last_requestid = i-1;
890                     break;
891                 }
892             }
893             if (i >= hs->id_wrap) {
894                 log_module(HS_LOG, LOG_INFO, "%s has more requests than its id_wrap.", hs->helpserv->nick);
895             }
896         }
897     }
898
899     req->hs = hs;
900     req->helper = NULL;
901     req->text = alloc_string_list(4);
902     req->user = user;
903     req->handle = user->handle_info;
904     if (from_join && self->burst) {
905         extern unsigned long burst_begin;
906         /* We need to keep all the requests during a burst join together,
907          * even if the burst takes more than 1 second. ircu seems to burst
908          * in reverse-join order. */
909         req->opened = burst_begin;
910     } else {
911         req->opened = now;
912     }
913     req->updated = now;
914
915     if (!hs->unhandled) {
916         hs->unhandled = req;
917         req->next_unhandled = NULL;
918     } else if (self->burst && hs->unhandled->opened >= req->opened) {
919         req->next_unhandled = hs->unhandled;
920         hs->unhandled = req;
921     } else if (self->burst) {
922         struct helpserv_request *unh;
923         /* Add the request to the beginning of the set of requests with
924          * req->opened having the same value. This makes reqonjoin create
925          * requests in the correct order while bursting. Note that this
926          * does not correct request ids, so they will be in reverse order
927          * though "/msg botname next" will work properly. */
928         for (unh = hs->unhandled; unh->next_unhandled && unh->next_unhandled->opened < req->opened; unh = unh->next_unhandled) ;
929         req->next_unhandled = unh->next_unhandled;
930         unh->next_unhandled = req;
931     } else {
932         struct helpserv_request *unh;
933         /* Add to the end */
934         for (unh = hs->unhandled; unh->next_unhandled; unh = unh->next_unhandled) ;
935         req->next_unhandled = NULL;
936         unh->next_unhandled = req;
937     }
938
939     if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
940         reqlist = helpserv_reqlist_alloc();
941         dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
942     }
943     req->parent_nick_list = reqlist;
944     helpserv_reqlist_append(reqlist, req);
945
946     if (user->handle_info) {
947         if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) {
948             hand_reqlist = helpserv_reqlist_alloc();
949             dict_insert(helpserv_reqs_byhand_dict, user->handle_info->handle, hand_reqlist);
950         }
951         req->parent_hand_list = hand_reqlist;
952         helpserv_reqlist_append(hand_reqlist, req);
953     } else {
954         req->parent_hand_list = NULL;
955     }
956
957     if (from_join) {
958         fmt = user_find_message(user, "HSMSG_REQ_NEWONJOIN");
959         sprintf(lbuf[0], fmt, hs->helpchan->name, req->id);
960     } else {
961         fmt = user_find_message(user, "HSMSG_REQ_NEW");
962         sprintf(lbuf[0], fmt, req->id);
963     }
964     if (req != hs->unhandled) {
965         intervalString(req_id, now - hs->unhandled->opened, user->handle_info);
966         fmt = user_find_message(user, "HSMSG_REQ_UNHANDLED_TIME");
967         sprintf(lbuf[1], fmt, req_id);
968     } else {
969         fmt = user_find_message(user, "HSMSG_REQ_NO_UNHANDLED");
970         sprintf(lbuf[1], "%s", fmt);
971     }
972     switch (hs->persist_lengths[PERSIST_T_REQUEST]) {
973         case PERSIST_PART:
974             fmt = user_find_message(user, "HSMSG_REQ_PERSIST_PART");
975             sprintf(lbuf[2], fmt, hs->helpchan->name, hs->helpchan->name);
976             break;
977         case PERSIST_QUIT:
978             fmt = user_find_message(user, "HSMSG_REQ_PERSIST_QUIT");
979             sprintf(lbuf[2], "%s", fmt);
980             break;
981         default:
982             log_module(HS_LOG, LOG_ERROR, "%s has an invalid req_persist.", hs->helpserv->nick);
983         case PERSIST_CLOSE:
984             if (user->handle_info) {
985                 fmt = user_find_message(user, "HSMSG_REQ_PERSIST_HANDLE");
986             } else {
987                 fmt = user_find_message(user, "HSMSG_REQ_PERSIST_QUIT");
988             }
989             sprintf(lbuf[2], "%s", fmt);
990             break;
991     }
992     helpserv_message(hs, user, MSGTYPE_REQ_OPENED);
993     if (from_opserv)
994         send_message_type(4, user, opserv, "%s %s %s", lbuf[0], lbuf[1], lbuf[2]);
995     else
996         send_message_type(4, user, hs->helpserv, "%s %s %s", lbuf[0], lbuf[1], lbuf[2]);
997
998     if (hs->req_on_join && req == hs->unhandled && hs->helpchan_empty && !user->uplink->burst) {
999         timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
1000         run_empty_interval(hs);
1001     }
1002
1003     return req;
1004 }
1005
1006 /* Handle a message from a user to a HelpServ bot. */
1007 static void helpserv_usermsg(struct userNode *user, struct helpserv_bot *hs, const char *text) {
1008     const int from_opserv = 0; /* for helpserv_notice */
1009     struct helpserv_request *req=NULL, *newest=NULL;
1010     struct helpserv_reqlist *reqlist, *hand_reqlist;
1011     unsigned int i;
1012
1013     if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
1014         for (i=0; i < reqlist->used; i++) {
1015             req = reqlist->list[i];
1016             if (req->hs != hs)
1017                 continue;
1018             if (!newest || (newest->opened < req->opened))
1019                 newest = req;
1020         }
1021
1022         /* If nothing was found, this will set req to NULL */
1023         req = newest;
1024     }
1025
1026     if (user->handle_info) {
1027         hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL);
1028         if (!req && hand_reqlist) {
1029             /* Most recent first again */
1030             for (i=0; i < hand_reqlist->used; i++) {
1031                 req = hand_reqlist->list[i];
1032                 if ((req->hs != hs) || req->user)
1033                     continue;
1034                 if (!newest || (newest->opened < req->opened))
1035                     newest = req;
1036             }
1037             req = newest;
1038
1039             if (req) {
1040                 req->user = user;
1041                 if (!reqlist) {
1042                     reqlist = helpserv_reqlist_alloc();
1043                     dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
1044                 }
1045                 req->parent_nick_list = reqlist;
1046                 helpserv_reqlist_append(reqlist, req);
1047
1048                 if (req->helper && (hs->notify >= NOTIFY_USER))
1049                     helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_FOUND", req->id, user->nick);
1050
1051                 helpserv_msguser(user, "HSMSG_GREET_PRIVMSG_EXISTREQ", req->id);
1052             }
1053         }
1054     } else {
1055         hand_reqlist = NULL;
1056     }
1057
1058     if (!req) {
1059         if (text[0] == helpserv_conf.user_escape) {
1060             helpserv_msguser(user, "HSMSG_USERCMD_NO_REQUEST");
1061             return;
1062         }
1063         if ((hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_PART) && !GetUserMode(hs->helpchan, user)) {
1064             helpserv_msguser(user, "HSMSG_REQ_YOU_NOT_IN_HELPCHAN_OPEN", hs->helpchan->name);
1065             return;
1066         }
1067
1068         req = create_request(user, hs, 0);
1069         if (user->handle_info)
1070             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_NEW_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle);
1071         else
1072             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_NEW_REQUEST_UNAUTHED", req->id, user->nick);
1073     } else if (text[0] == helpserv_conf.user_escape) {
1074         char cmdname[MAXLEN], *space;
1075         helpserv_usercmd_t *usercmd;
1076         struct userNode *likely_helper;
1077
1078         /* Find somebody likely to be the helper */
1079         if (!req->helper)
1080             likely_helper = NULL;
1081         else if ((likely_helper = req->helper->handle->users) && !likely_helper->next_authed) {
1082             /* only one user it could be :> */
1083         } else for (likely_helper = req->helper->handle->users; likely_helper; likely_helper = likely_helper->next_authed)
1084             if (GetUserMode(hs->helpchan, likely_helper))
1085                 break;
1086
1087         /* Parse out command name */
1088         space = strchr(text+1, ' ');
1089         if (space)
1090             strncpy(cmdname, text+1, space-text-1);
1091         else
1092             strcpy(cmdname, text+1);
1093
1094         /* Call the user command function */
1095         usercmd = dict_find(helpserv_usercmd_dict, cmdname, NULL);
1096         if (usercmd)
1097             usercmd(req, likely_helper, space+1);
1098         else
1099             helpserv_msguser(user, "HSMSG_USERCMD_UNKNOWN", cmdname);
1100         return;
1101     } else if (hs->intervals[INTERVAL_STALE_DELAY]
1102                && (req->updated < now - hs->intervals[INTERVAL_STALE_DELAY])
1103                && (!hs->req_maxlen || req->text->used < hs->req_maxlen)) {
1104         char buf[MAX_LINE_SIZE], updatestr[INTERVALLEN], timestr[MAX_LINE_SIZE];
1105         time_t feh;
1106
1107         feh = req->opened;
1108         strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&feh));
1109         intervalString(updatestr, now - req->updated, user->handle_info);
1110         if (req->helper && (hs->notify >= NOTIFY_USER))
1111             if (user->handle_info)
1112                 helpserv_notify(req->helper, "HSMSG_PAGE_UPD_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle, timestr, updatestr);
1113             else
1114                 helpserv_notify(req->helper, "HSMSG_PAGE_UPD_REQUEST_NOT_AUTHED", req->id, user->nick, timestr, updatestr);
1115         else
1116             if (user->handle_info)
1117                 helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_UPD_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle, timestr, updatestr);
1118             else
1119                 helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_UPD_REQUEST_NOT_AUTHED", req->id, user->nick, timestr, updatestr);
1120         feh = now;
1121         strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&feh));
1122         snprintf(buf, MAX_LINE_SIZE, "[Stale request updated at %s]", timestr);
1123         string_list_append(req->text, strdup(buf));
1124     }
1125
1126     req->updated = now;
1127     if (!hs->req_maxlen || req->text->used < hs->req_maxlen) {
1128         struct userNode *likely_helper;
1129
1130         string_list_append(req->text, strdup(text));
1131         /* Find somebody likely to be the helper */
1132         if (!req->helper)
1133             likely_helper = NULL;
1134         else if ((likely_helper = req->helper->handle->users) && !likely_helper->next_authed) {
1135             /* only one user it could be :> */
1136         } else for (likely_helper = req->helper->handle->users; likely_helper; likely_helper = likely_helper->next_authed)
1137             if (GetUserMode(hs->helpchan, likely_helper))
1138                 break;
1139
1140         if(likely_helper)
1141             send_target_message(1, likely_helper->nick, hs->helpserv, "HSMSQ_REQ_TEXT_ADDED", user->nick, text);
1142
1143     } else
1144         helpserv_msguser(user, "HSMSG_REQ_MAXLEN");
1145 }
1146
1147 /* Handle messages direct to a HelpServ bot. */
1148 static void helpserv_botmsg(struct userNode *user, struct userNode *target, const char *text, UNUSED_ARG(int server_qualified)) {
1149     struct helpserv_bot *hs;
1150     struct helpserv_cmd *cmd;
1151     struct helpserv_user *hs_user;
1152     char *argv[MAXNUMPARAMS];
1153     char tmpline[MAXLEN];
1154     int argc, argv_shift;
1155     const int from_opserv = 0; /* for helpserv_notice */
1156
1157     /* Ignore things consisting of empty lines or from ourselves */
1158     if (!*text || IsLocal(user))
1159         return;
1160
1161     hs = dict_find(helpserv_bots_dict, target->nick, NULL);
1162
1163     /* See if we should listen to their message as a command (helper)
1164      * or a help request (user) */
1165     if (!user->handle_info || !(hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
1166         helpserv_usermsg(user, hs, text);
1167         return;
1168     }
1169
1170     argv_shift = 1;
1171     safestrncpy(tmpline, text, sizeof(tmpline));
1172     argc = split_line(tmpline, false, ArrayLength(argv)-argv_shift, argv+argv_shift);
1173     if (!argc)
1174         return;
1175
1176     cmd = dict_find(helpserv_func_dict, argv[argv_shift], NULL);
1177     if (!cmd) {
1178         helpserv_notice(user, "MSG_COMMAND_UNKNOWN", argv[argv_shift]);
1179         return;
1180     }
1181     if (cmd->flags & CMD_FROM_OPSERV_ONLY) {
1182         helpserv_notice(user, "HSMSG_OPER_CMD");
1183         return;
1184     }
1185     if (cmd->access > hs_user->level) {
1186         helpserv_notice(user, "HSMSG_LEVEL_TOO_LOW");
1187         return;
1188     }
1189     if (!cmd->func) {
1190         helpserv_notice(user, "HSMSG_INTERNAL_COMMAND", argv[argv_shift]);
1191     } else if (cmd->func(user, hs, 0, argc, argv+argv_shift)) {
1192         unsplit_string(argv+argv_shift, argc, tmpline);
1193         log_audit(HS_LOG, LOG_COMMAND, user, hs->helpserv, hs->helpchan->name, 0, tmpline);
1194     }
1195 }
1196
1197 /* Handle a control command from an IRC operator */
1198 static MODCMD_FUNC(cmd_helpserv) {
1199     struct helpserv_bot *hs = NULL;
1200     struct helpserv_cmd *subcmd;
1201     const int from_opserv = 1; /* for helpserv_notice */
1202     char botnick[NICKLEN+1]; /* in case command is unregister */
1203     int retval;
1204
1205     if (argc < 2) {
1206         send_help(user, opserv, helpserv_helpfile, NULL);
1207         return 0;
1208     }
1209
1210     if (!(subcmd = dict_find(helpserv_func_dict, argv[1], NULL))) {
1211         helpserv_notice(user, "MSG_COMMAND_UNKNOWN", argv[1]);
1212         return 0;
1213     }
1214
1215     if (!subcmd->func) {
1216         helpserv_notice(user, "HSMSG_INTERNAL_COMMAND", argv[1]);
1217         return 0;
1218     }
1219
1220     if ((subcmd->flags & CMD_NEED_BOT) && ((argc < 3) || !(hs = dict_find(helpserv_bots_dict, argv[2], NULL)))) {
1221         helpserv_notice(user, "HSMSG_INVALID_BOT");
1222         return 0;
1223     }
1224
1225     if (subcmd->flags & CMD_NEVER_FROM_OPSERV) {
1226         helpserv_notice(user, "HSMSG_NO_USE_OPSERV");
1227         return 0;
1228     }
1229
1230     if (hs) {
1231         argv[2] = argv[1];
1232         strcpy(botnick, hs->helpserv->nick);
1233         retval = subcmd->func(user, hs, 1, argc-2, argv+2);
1234     } else {
1235         strcpy(botnick, "No bot");
1236         retval = subcmd->func(user, hs, 1, argc-1, argv+1);
1237     }
1238
1239     return retval;
1240 }
1241
1242 static void helpserv_help(struct helpserv_bot *hs, int from_opserv, struct userNode *user, const char *topic) {
1243     send_help(user, (from_opserv ? opserv : hs->helpserv), helpserv_helpfile, topic);
1244 }
1245
1246 static int append_entry(const char *key, UNUSED_ARG(void *data), void *extra) {
1247     struct helpfile_expansion *exp = extra;
1248     int row;
1249
1250     row = exp->value.table.length++;
1251     exp->value.table.contents[row] = calloc(1, sizeof(char*));
1252     exp->value.table.contents[row][0] = key;
1253     return 0;
1254 }
1255
1256 static struct helpfile_expansion helpserv_expand_variable(const char *variable) {
1257     struct helpfile_expansion exp;
1258
1259     if (!irccasecmp(variable, "index")) {
1260         exp.type = HF_TABLE;
1261         exp.value.table.length = 1;
1262         exp.value.table.width = 1;
1263         exp.value.table.flags = TABLE_REPEAT_ROWS;
1264         exp.value.table.contents = calloc(dict_size(helpserv_func_dict)+1, sizeof(char**));
1265         exp.value.table.contents[0] = calloc(1, sizeof(char*));
1266         exp.value.table.contents[0][0] = "Commands:";
1267         dict_foreach(helpserv_func_dict, append_entry, &exp);
1268         return exp;
1269     }
1270
1271     exp.type = HF_STRING;
1272     exp.value.str = NULL;
1273     return exp;
1274 }
1275
1276 static void helpserv_helpfile_read(void) {
1277     helpserv_helpfile = open_helpfile(HELPSERV_HELPFILE_NAME, helpserv_expand_variable);
1278 }
1279
1280 static HELPSERV_USERCMD(usercmd_wait) {
1281     struct helpserv_request *other;
1282     int pos, count;
1283     char buf[INTERVALLEN];
1284
1285     if (req->helper) {
1286         if (likely_helper)
1287             helpserv_user_reply("HSMSG_YOU_BEING_HELPED_BY", likely_helper->nick);
1288         else
1289             helpserv_user_reply("HSMSG_YOU_BEING_HELPED");
1290         return;
1291     }
1292
1293     for (other = req->hs->unhandled, pos = -1, count = 0;
1294          other;
1295          other = other->next_unhandled, ++count) {
1296         if (other == req)
1297             pos = count;
1298     }
1299     assert(pos >= 0);
1300     intervalString(buf, now - req->hs->unhandled->opened, req->user->handle_info);
1301     helpserv_user_reply("HSMSG_WAIT_STATUS", pos+1, count, buf);
1302 }
1303
1304 static HELPSERV_FUNC(cmd_help) {
1305     const char *topic;
1306
1307     if (argc < 2)
1308         topic = NULL;
1309     else
1310         topic = unsplit_string(argv+1, argc-1, NULL);
1311     helpserv_help(hs, from_opserv, user, topic);
1312
1313     return 1;
1314 }
1315
1316 static HELPSERV_FUNC(cmd_readhelp) {
1317     struct timeval start, stop;
1318     struct helpfile *old_helpfile = helpserv_helpfile;
1319
1320     gettimeofday(&start, NULL);
1321     helpserv_helpfile_read();
1322     if (helpserv_helpfile) {
1323         close_helpfile(old_helpfile);
1324     } else {
1325         helpserv_helpfile = old_helpfile;
1326     }
1327     gettimeofday(&stop, NULL);
1328     stop.tv_sec -= start.tv_sec;
1329     stop.tv_usec -= start.tv_usec;
1330     if (stop.tv_usec < 0) {
1331         stop.tv_sec -= 1;
1332         stop.tv_usec += 1000000;
1333     }
1334     helpserv_notice(user, "HSMSG_READHELP_SUCCESS", (unsigned long)stop.tv_sec, (unsigned long)stop.tv_usec/1000);
1335
1336     return 1;
1337 }
1338
1339 static struct helpserv_user * helpserv_add_user(struct helpserv_bot *hs, struct handle_info *handle, enum helpserv_level level) {
1340     struct helpserv_user *hs_user;
1341     struct helpserv_userlist *userlist;
1342
1343     hs_user = calloc(1, sizeof(struct helpserv_user));
1344     hs_user->handle = handle;
1345     hs_user->hs = hs;
1346     hs_user->help_mode = 0;
1347     hs_user->level = level;
1348     hs_user->join_time = find_handle_in_channel(hs->helpchan, handle, NULL) ? now : 0;
1349     dict_insert(hs->users, handle->handle, hs_user);
1350
1351     if (!(userlist = dict_find(helpserv_users_byhand_dict, handle->handle, NULL))) {
1352         userlist = helpserv_userlist_alloc();
1353         dict_insert(helpserv_users_byhand_dict, handle->handle, userlist);
1354     }
1355     helpserv_userlist_append(userlist, hs_user);
1356
1357     return hs_user;
1358 }
1359
1360 static void helpserv_del_user(struct helpserv_bot *hs, struct helpserv_user *hs_user) {
1361     dict_remove(hs->users, hs_user->handle->handle);
1362 }
1363
1364 static int cmd_add_user(struct helpserv_bot *hs, int from_opserv, struct userNode *user, enum helpserv_level level, int argc, char *argv[]) {
1365     struct helpserv_user *actor;
1366     struct handle_info *handle;
1367
1368     REQUIRE_PARMS(2);
1369
1370     if (!from_opserv) {
1371         actor = GetHSUser(hs, user->handle_info);
1372         if (actor->level < HlManager) {
1373             helpserv_notice(user, "HSMSG_CANNOT_ADD");
1374             return 0;
1375         }
1376     } else {
1377         actor = NULL;
1378     }
1379
1380     if (!(handle = helpserv_get_handle_info(user, argv[1])))
1381         return 0;
1382
1383     if (GetHSUser(hs, handle)) {
1384         helpserv_notice(user, "HSMSG_USER_EXISTS", handle->handle);
1385         return 0;
1386     }
1387
1388     if (!(from_opserv) && actor && (actor->level <= level)) {
1389         helpserv_notice(user, "HSMSG_NO_BUMP_ACCESS");
1390         return 0;
1391     }
1392
1393     helpserv_add_user(hs, handle, level);
1394
1395     helpserv_notice(user, "HSMSG_ADDED_USER", helpserv_level2str(level), handle->handle);
1396     return 1;
1397 }
1398
1399 static HELPSERV_FUNC(cmd_deluser) {
1400     struct helpserv_user *actor=NULL, *victim;
1401     struct handle_info *handle;
1402     enum helpserv_level level;
1403
1404     REQUIRE_PARMS(2);
1405
1406     if (!from_opserv) {
1407         actor = GetHSUser(hs, user->handle_info);
1408         if (actor->level < HlManager) {
1409             helpserv_notice(user, "HSMSG_CANNOT_DEL");
1410             return 0;
1411         }
1412     }
1413
1414     if (!(handle = helpserv_get_handle_info(user, argv[1])))
1415         return 0;
1416
1417     if (!(victim = GetHSUser(hs, handle))) {
1418         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", handle->handle, hs->helpserv->nick);
1419         return 0;
1420     }
1421
1422     if (!from_opserv && actor && (actor->level <= victim->level)) {
1423         helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
1424         return 0;
1425     }
1426
1427     level = victim->level;
1428     helpserv_del_user(hs, victim);
1429     helpserv_notice(user, "HSMSG_DELETED_USER", helpserv_level2str(level), handle->handle);
1430     return 1;
1431 }
1432
1433 static int
1434 helpserv_user_comp(const void *arg_a, const void *arg_b)
1435 {
1436     const struct helpserv_user *a = *(struct helpserv_user**)arg_a;
1437     const struct helpserv_user *b = *(struct helpserv_user**)arg_b;
1438     int res;
1439     if (a->level != b->level)
1440         res = b->level - a->level;
1441     else
1442         res = irccasecmp(a->handle->handle, b->handle->handle);
1443     return res;
1444 }
1445
1446 static int show_helper_range(struct userNode *user, struct helpserv_bot *hs, int from_opserv, enum helpserv_level min_lvl, enum helpserv_level max_lvl) {
1447     struct helpserv_userlist users;
1448     struct helpfile_table tbl;
1449     struct helpserv_user *hs_user;
1450     dict_iterator_t it;
1451     enum helpserv_level last_level;
1452     unsigned int ii;
1453
1454     users.used = 0;
1455     users.size = dict_size(hs->users);
1456     users.list = alloca(users.size*sizeof(hs->users[0]));
1457     helpserv_notice(user, "HSMSG_USERLIST_HEADER", hs->helpserv->nick);
1458     for (it = dict_first(hs->users); it; it = iter_next(it)) {
1459         hs_user = iter_data(it);
1460         if (hs_user->level < min_lvl)
1461             continue;
1462         if (hs_user->level > max_lvl)
1463             continue;
1464         users.list[users.used++] = hs_user;
1465     }
1466     if (!users.used) {
1467         helpserv_notice(user, "MSG_NONE");
1468         return 1;
1469     }
1470     qsort(users.list, users.used, sizeof(users.list[0]), helpserv_user_comp);
1471     switch (user->handle_info->userlist_style) {
1472     case HI_STYLE_DEF:
1473         tbl.length = users.used + 1;
1474         tbl.width = 3;
1475         tbl.flags = TABLE_NO_FREE;
1476         tbl.contents = alloca(tbl.length * sizeof(tbl.contents[0]));
1477         tbl.contents[0] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
1478         tbl.contents[0][0] = "Level";
1479         tbl.contents[0][1] = "Handle";
1480         tbl.contents[0][2] = "WeekStart";
1481         for (ii = 0; ii < users.used; ) {
1482             hs_user = users.list[ii++];
1483             tbl.contents[ii] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
1484             tbl.contents[ii][0] = helpserv_level_names[hs_user->level];
1485             tbl.contents[ii][1] = hs_user->handle->handle;
1486             tbl.contents[ii][2] = weekday_names[hs_user->week_start];
1487         }
1488         table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
1489         break;
1490     case HI_STYLE_ZOOT: default:
1491         last_level = HlNone;
1492         tbl.length = 0;
1493         tbl.width = 1;
1494         tbl.flags = TABLE_NO_FREE | TABLE_REPEAT_ROWS | TABLE_NO_HEADERS;
1495         tbl.contents = alloca(users.used * sizeof(tbl.contents[0]));
1496         for (ii = 0; ii < users.used; ) {
1497             hs_user = users.list[ii++];
1498             if (hs_user->level != last_level) {
1499                 if (tbl.length) {
1500                     helpserv_notice(user, "HSMSG_USERLIST_ZOOT_LVL", hs->helpserv->nick, helpserv_level_names[last_level]);
1501                     table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
1502                     tbl.length = 0;
1503                 }
1504                 last_level = hs_user->level;
1505             }
1506             tbl.contents[tbl.length] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
1507             tbl.contents[tbl.length++][0] = hs_user->handle->handle;
1508         }
1509         if (tbl.length) {
1510             helpserv_notice(user, "HSMSG_USERLIST_ZOOT_LVL", hs->helpserv->nick, helpserv_level_names[last_level]);
1511             table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
1512         }
1513     }
1514     return 1;
1515 }
1516
1517 static HELPSERV_FUNC(cmd_helpers) {
1518     return show_helper_range(user, hs, from_opserv, HlTrial, HlOwner);
1519 }
1520
1521 static HELPSERV_FUNC(cmd_wlist) {
1522     return show_helper_range(user, hs, from_opserv, HlOwner, HlOwner);
1523 }
1524
1525 static HELPSERV_FUNC(cmd_mlist) {
1526     return show_helper_range(user, hs, from_opserv, HlManager, HlManager);
1527 }
1528
1529 static HELPSERV_FUNC(cmd_hlist) {
1530     return show_helper_range(user, hs, from_opserv, HlHelper, HlHelper);
1531 }
1532
1533 static HELPSERV_FUNC(cmd_tlist) {
1534     return show_helper_range(user, hs, from_opserv, HlTrial, HlTrial);
1535 }
1536
1537 static HELPSERV_FUNC(cmd_addowner) {
1538     return cmd_add_user(hs, from_opserv, user, HlOwner, argc, argv);
1539 }
1540
1541 static HELPSERV_FUNC(cmd_addmanager) {
1542     return cmd_add_user(hs, from_opserv, user, HlManager, argc, argv);
1543 }
1544
1545 static HELPSERV_FUNC(cmd_addhelper) {
1546     return cmd_add_user(hs, from_opserv, user, HlHelper, argc, argv);
1547 }
1548
1549 static HELPSERV_FUNC(cmd_addtrial) {
1550     return cmd_add_user(hs, from_opserv, user, HlTrial, argc, argv);
1551 }
1552
1553 static HELPSERV_FUNC(cmd_clvl) {
1554     struct helpserv_user *actor=NULL, *victim;
1555     struct handle_info *handle;
1556     enum helpserv_level level;
1557
1558     REQUIRE_PARMS(3);
1559
1560     if (!from_opserv) {
1561         actor = GetHSUser(hs, user->handle_info);
1562         if (actor->level < HlManager) {
1563             helpserv_notice(user, "HSMSG_CANNOT_CLVL");
1564             return 0;
1565         }
1566     }
1567
1568     if (!(handle = helpserv_get_handle_info(user, argv[1])))
1569         return 0;
1570
1571     if (!(victim = GetHSUser(hs, handle))) {
1572         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", handle->handle, hs->helpserv->nick);
1573         return 0;
1574     }
1575
1576     if (((level = helpserv_str2level(argv[2])) == HlNone) || level == HlOper) {
1577         helpserv_notice(user, "HSMSG_INVALID_ACCESS", argv[2]);
1578         return 0;
1579     }
1580
1581     if (!(from_opserv) && actor) {
1582         if (actor == victim) {
1583             helpserv_notice(user, "HSMSG_NO_SELF_CLVL");
1584             return 0;
1585         }
1586
1587         if (actor->level <= victim->level) {
1588             helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
1589             return 0;
1590         }
1591
1592         if (level >= actor->level) {
1593             helpserv_notice(user, "HSMSG_NO_BUMP_ACCESS");
1594             return 0;
1595         }
1596     }
1597
1598     victim->level = level;
1599     helpserv_notice(user, "HSMSG_CHANGED_ACCESS", handle->handle, helpserv_level2str(level));
1600
1601     return 1;
1602 }
1603
1604 static void free_request(void *data) {
1605     struct helpserv_request *req = data;
1606
1607     /* Logging */
1608     if (shutting_down && (req->hs->persist_lengths[PERSIST_T_REQUEST] != PERSIST_CLOSE || !req->handle)) {
1609         helpserv_log_request(req, "srvx shutdown");
1610     }
1611
1612     /* Clean up from the unhandled queue */
1613     if (req->hs->unhandled) {
1614         if (req->hs->unhandled == req) {
1615             req->hs->unhandled = req->next_unhandled;
1616         } else {
1617             struct helpserv_request *uh;
1618             for (uh=req->hs->unhandled; uh->next_unhandled && (uh->next_unhandled != req); uh = uh->next_unhandled);
1619             if (uh->next_unhandled) {
1620                 uh->next_unhandled = req->next_unhandled;
1621             }
1622         }
1623     }
1624
1625     /* Clean up the lists */
1626     if (req->parent_nick_list) {
1627         if (req->parent_nick_list->used == 1) {
1628             dict_remove(helpserv_reqs_bynick_dict, req->user->nick);
1629         } else {
1630             helpserv_reqlist_remove(req->parent_nick_list, req);
1631         }
1632     }
1633     if (req->parent_hand_list) {
1634         if (req->parent_hand_list->used == 1) {
1635             dict_remove(helpserv_reqs_byhand_dict, req->handle->handle);
1636         } else {
1637             helpserv_reqlist_remove(req->parent_hand_list, req);
1638         }
1639     }
1640
1641     free_string_list(req->text);
1642     free(req);
1643 }
1644
1645 static HELPSERV_FUNC(cmd_close) {
1646     struct helpserv_request *req, *newest=NULL;
1647     struct helpserv_reqlist *nick_list, *hand_list;
1648     struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
1649     struct userNode *req_user=NULL;
1650     char close_reason[MAXLEN], reqnum[12];
1651     unsigned long old_req;
1652     unsigned int i;
1653     int num_requests=0;
1654
1655     REQUIRE_PARMS(2);
1656
1657     assert(hs_user);
1658
1659     if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
1660         helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
1661         return 0;
1662     }
1663
1664     sprintf(reqnum, "%lu", req->id);
1665
1666     if (num_requests > 1)
1667         helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
1668
1669     if (hs_user->level < HlManager && req->helper != hs_user) {
1670         if (req->helper)
1671             helpserv_notice(user, "HSMSG_REQ_NOT_YOURS_ASSIGNED_TO", req->id, req->helper->handle->handle);
1672         else
1673             helpserv_notice(user, "HSMSG_REQ_NOT_YOURS_UNASSIGNED", req->id);
1674         return 0;
1675     }
1676
1677     helpserv_notice(user, "HSMSG_REQ_CLOSED", req->id);
1678     if (req->user) {
1679         req_user = req->user;
1680         helpserv_message(hs, req->user, MSGTYPE_REQ_CLOSED);
1681         if (req->handle)
1682             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_1", req->id, req->user->nick, req->handle->handle, user->nick);
1683         else
1684             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_2", req->id, req->user->nick, user->nick);
1685     } else {
1686         if (req->handle)
1687             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_3", req->id, req->handle->handle, user->nick);
1688         else
1689             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_4", req->id, user->nick);
1690     }
1691
1692     hs_user->closed[0]++;
1693     hs_user->closed[4]++;
1694
1695     /* Set these to keep track of the lists after the request is gone, but
1696      * not if free_request() will helpserv_reqlist_free() them. */
1697     nick_list = req->parent_nick_list;
1698     if (nick_list && (nick_list->used == 1))
1699         nick_list = NULL;
1700     hand_list = req->parent_hand_list;
1701     if (hand_list && (hand_list->used == 1))
1702         hand_list = NULL;
1703     old_req = req->id;
1704
1705     if (argc >= 3) {
1706         snprintf(close_reason, MAXLEN, "Closed by %s: %s", user->handle_info->handle, unsplit_string(argv+2, argc-2, NULL));
1707     } else {
1708         sprintf(close_reason, "Closed by %s", user->handle_info->handle);
1709     }
1710     helpserv_log_request(req, close_reason);
1711     dict_remove(hs->requests, reqnum);
1712
1713     /* Look for other requests associated with them */
1714     if (nick_list) {
1715         for (i=0; i < nick_list->used; i++) {
1716             req = nick_list->list[i];
1717
1718             if (req->hs != hs)
1719                 continue;
1720             if (!newest || (newest->opened < req->opened))
1721                 newest = req;
1722         }
1723
1724         if (newest)
1725             helpserv_msguser(newest->user, "HSMSG_REQ_FOUND_ANOTHER", old_req, newest->id);
1726     }
1727
1728     if (req_user && hs->auto_devoice) {
1729         struct modeNode *mn = GetUserMode(hs->helpchan, req_user);
1730         if ((!newest || !newest->helper) && mn && (mn->modes & MODE_VOICE)) {
1731             struct mod_chanmode change;
1732             mod_chanmode_init(&change);
1733             change.argc = 1;
1734             change.args[0].mode = MODE_REMOVE | MODE_VOICE;
1735             change.args[0].u.member = mn;
1736             mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
1737         }
1738     }
1739
1740     return 1;
1741 }
1742
1743 static HELPSERV_FUNC(cmd_list) {
1744     dict_iterator_t it;
1745     int searchtype;
1746     struct helpfile_table tbl;
1747     unsigned int line, total;
1748     struct helpserv_request *req;
1749
1750     if ((argc < 2) || !irccasecmp(argv[1], "unassigned")) {
1751         for (req = hs->unhandled, total=0; req; req = req->next_unhandled, total++) ;
1752         helpserv_notice(user, "HSMSG_REQ_LIST_TOP_UNASSIGNED", total);
1753         searchtype = 1; /* Unassigned */
1754     } else if (!irccasecmp(argv[1], "assigned")) {
1755         for (req = hs->unhandled, total=dict_size(hs->requests); req; req = req->next_unhandled, total--) ;
1756         helpserv_notice(user, "HSMSG_REQ_LIST_TOP_ASSIGNED", total);
1757         searchtype = 2; /* Assigned */
1758     } else if (!irccasecmp(argv[1], "me")) {
1759         for (total = 0, it = dict_first(hs->requests); it; it = iter_next(it)) {
1760             req = iter_data(it);
1761             if (req->helper && (req->helper->handle == user->handle_info))
1762                 total++;
1763         }
1764         helpserv_notice(user, "HSMSG_REQ_LIST_TOP_YOUR", total);
1765         searchtype = 4;
1766     } else if (!irccasecmp(argv[1], "all")) {
1767         total = dict_size(hs->requests);
1768         helpserv_notice(user, "HSMSG_REQ_LIST_TOP_ALL", total);
1769         searchtype = 3; /* All */
1770     } else {
1771         helpserv_notice(user, "HSMSG_BAD_REQ_TYPE", argv[1]);
1772         return 0;
1773     }
1774
1775     if (!total) {
1776         helpserv_notice(user, "HSMSG_REQ_LIST_NONE");
1777         return 1;
1778     }
1779
1780     tbl.length = total+1;
1781     tbl.width = 5;
1782     tbl.flags = TABLE_NO_FREE;
1783     tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
1784     tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
1785     tbl.contents[0][0] = "ID#";
1786     tbl.contents[0][1] = "User";
1787     tbl.contents[0][2] = "Helper";
1788     tbl.contents[0][3] = "Time open";
1789     tbl.contents[0][4] = "User status";
1790
1791     for (it=dict_first(hs->requests), line=0; it; it=iter_next(it)) {
1792         char opentime[INTERVALLEN], reqid[12], username[NICKLEN+2];
1793
1794         req = iter_data(it);
1795
1796         switch (searchtype) {
1797         case 1:
1798             if (req->helper)
1799                 continue;
1800             break;
1801         case 2:
1802             if (!req->helper)
1803                 continue;
1804             break;
1805         case 3:
1806         default:
1807             break;
1808         case 4:
1809             if (!req->helper || (req->helper->handle != user->handle_info))
1810                 continue;
1811             break;
1812         }
1813
1814         line++;
1815
1816         tbl.contents[line] = alloca(tbl.width * sizeof(**tbl.contents));
1817         sprintf(reqid, "%lu", req->id);
1818         tbl.contents[line][0] = strdup(reqid);
1819         if (req->user) {
1820             strcpy(username, req->user->nick);
1821         } else {
1822             username[0] = '*';
1823             strcpy(username+1, req->handle->handle);
1824         }
1825         tbl.contents[line][1] = strdup(username);
1826         tbl.contents[line][2] = req->helper ? req->helper->handle->handle : "(Unassigned)";
1827         intervalString(opentime, now - req->opened, user->handle_info);
1828         tbl.contents[line][3] = strdup(opentime);
1829         tbl.contents[line][4] = ((req->user || req->handle->users) ? "Online" : "Offline");
1830     }
1831
1832     table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
1833
1834     for (; line > 0; line--) {
1835         free((char *)tbl.contents[line][0]);
1836         free((char *)tbl.contents[line][1]);
1837         free((char *)tbl.contents[line][3]);
1838     }
1839
1840     return 1;
1841 }
1842
1843 static void helpserv_show(int from_opserv, struct helpserv_bot *hs, struct userNode *user, struct helpserv_request *req) {
1844     unsigned int nn;
1845     char buf[MAX_LINE_SIZE];
1846     char buf2[INTERVALLEN];
1847     time_t feh;
1848
1849     if (req->user)
1850         if (req->handle)
1851             helpserv_notice(user, "HSMSG_REQ_INFO_2a", req->user->nick, req->handle->handle);
1852         else
1853             helpserv_notice(user, "HSMSG_REQ_INFO_2b", req->user->nick);
1854     else if (req->handle)
1855         if (req->handle->users)
1856             helpserv_notice(user, "HSMSG_REQ_INFO_2c", req->handle->handle);
1857         else
1858             helpserv_notice(user, "HSMSG_REQ_INFO_2d", req->handle->handle);
1859     else
1860         helpserv_notice(user, "HSMSG_REQ_INFO_2e");
1861     feh = req->opened;
1862     strftime(buf, MAX_LINE_SIZE, HSFMT_TIME, localtime(&feh));
1863     intervalString(buf2, now - req->opened, user->handle_info);
1864     helpserv_notice(user, "HSMSG_REQ_INFO_3", buf, buf2);
1865     helpserv_notice(user, "HSMSG_REQ_INFO_4");
1866     for (nn=0; nn < req->text->used; nn++)
1867         helpserv_notice(user, "HSMSG_REQ_INFO_MESSAGE", req->text->list[nn]);
1868 }
1869
1870 /* actor is the one who executed the command... it should == user except from
1871  * cmd_assign */
1872 static int helpserv_assign(int from_opserv, struct helpserv_bot *hs, struct userNode *user, struct userNode *actor, struct helpserv_request *req) {
1873     struct helpserv_request *req2;
1874     struct helpserv_user *old_helper;
1875
1876     if (!user->handle_info)
1877         return 0;
1878     if ((hs->persist_lengths[PERSIST_T_HELPER] == PERSIST_PART) && !GetUserMode(hs->helpchan, user)) {
1879         struct helpserv_user *hsuser_actor = GetHSUser(hs, actor->handle_info);
1880         if (hsuser_actor->level < HlManager) {
1881             helpserv_notice(user, "HSMSG_REQ_YOU_NOT_IN_HELPCHAN", hs->helpchan->name);
1882             return 0;
1883         } else if (user != actor) {
1884             helpserv_notice(user, "HSMSG_REQ_YOU_NOT_IN_OVERRIDE", hs->helpchan->name);
1885             helpserv_notice(actor, "HSMSG_REQ_HIM_NOT_IN_OVERRIDE", user->nick, hs->helpchan->name);
1886         } else
1887             helpserv_notice(user, "HSMSG_REQ_SELF_NOT_IN_OVERRIDE", hs->helpchan->name);
1888     }
1889
1890     hs->last_active = now;
1891     if ((old_helper = req->helper)) {
1892         /* don't need to remove from the unhandled queue */
1893     } else if (hs->unhandled == req) {
1894         hs->unhandled = req->next_unhandled;
1895     } else for (req2 = hs->unhandled; req2; req2 = req2->next_unhandled) {
1896         if (req2->next_unhandled == req) {
1897             req2->next_unhandled = req->next_unhandled;
1898             break;
1899         }
1900     }
1901     req->next_unhandled = NULL;
1902     req->helper = GetHSUser(hs, user->handle_info);
1903     assert(req->helper);
1904     req->assigned = now;
1905
1906     if (old_helper) {
1907         helpserv_notice(user, "HSMSG_REQ_REASSIGNED", req->id, old_helper->handle->handle);
1908         req->helper->reassigned_to[0]++;
1909         req->helper->reassigned_to[4]++;
1910         old_helper->reassigned_from[0]++;
1911         old_helper->reassigned_from[4]++;
1912     } else {
1913         helpserv_notice(user, "HSMSG_REQ_ASSIGNED_YOU", req->id);
1914         req->helper->picked_up[0]++;
1915         req->helper->picked_up[4]++;
1916     }
1917     helpserv_show(from_opserv, hs, user, req);
1918     if (req->user) {
1919         helpserv_message(hs, req->user, MSGTYPE_REQ_ASSIGNED);
1920         if (old_helper) {
1921             helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED_AGAIN", req->id, user->handle_info->handle, user->nick);
1922         } else {
1923             helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED", req->id, user->handle_info->handle, user->nick);
1924         }
1925         if (req->handle)
1926             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_1", req->id, req->user->nick, req->handle->handle, user->nick);
1927         else
1928             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_2", req->id, req->user->nick, user->nick);
1929     } else {
1930         if (req->handle)
1931             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_3", req->id, req->handle->handle, user->nick);
1932         else
1933             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_4", req->id, user->nick);
1934     }
1935
1936     if (req->user && hs->auto_voice) {
1937         struct mod_chanmode change;
1938         mod_chanmode_init(&change);
1939         change.argc = 1;
1940         change.args[0].mode = MODE_VOICE;
1941         if ((change.args[0].u.member = GetUserMode(hs->helpchan, req->user)))
1942             mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
1943     }
1944
1945     return 1;
1946 }
1947
1948 static HELPSERV_FUNC(cmd_next) {
1949     struct helpserv_request *req;
1950
1951     if (!(req = hs->unhandled)) {
1952         helpserv_notice(user, "HSMSG_REQ_NO_UNASSIGNED");
1953         return 0;
1954     }
1955     return helpserv_assign(from_opserv, hs, user, user, req);
1956 }
1957
1958 static HELPSERV_FUNC(cmd_show) {
1959     struct helpserv_request *req;
1960     struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
1961     int num_requests=0;
1962
1963     REQUIRE_PARMS(2);
1964
1965     if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
1966         helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
1967         return 0;
1968     }
1969
1970     if (num_requests > 1)
1971         helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
1972
1973     helpserv_notice(user, "HSMSG_REQ_INFO_1", req->id);
1974     helpserv_show(from_opserv, hs, user, req);
1975     return 1;
1976 }
1977
1978 static HELPSERV_FUNC(cmd_pickup) {
1979     struct helpserv_request *req;
1980     struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
1981     int num_requests=0;
1982
1983     REQUIRE_PARMS(2);
1984
1985     assert(hs_user);
1986
1987     if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
1988         helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
1989         return 0;
1990     }
1991
1992     if (num_requests > 1)
1993         helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
1994
1995     return helpserv_assign(from_opserv, hs, user, user, req);
1996 }
1997
1998 static HELPSERV_FUNC(cmd_reassign) {
1999     struct helpserv_request *req;
2000     struct userNode *targetuser;
2001     struct helpserv_user *target;
2002     struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
2003     int num_requests=0;
2004
2005     REQUIRE_PARMS(3);
2006
2007     assert(hs_user);
2008
2009     if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
2010         helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
2011         return 0;
2012     }
2013
2014     if (num_requests > 1)
2015         helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
2016
2017     if (!(targetuser = GetUserH(argv[2]))) {
2018         helpserv_notice(user, "MSG_NICK_UNKNOWN", argv[2]);
2019         return 0;
2020     }
2021
2022     if (!targetuser->handle_info) {
2023         helpserv_notice(user, "MSG_USER_AUTHENTICATE", targetuser->nick);
2024         return 0;
2025     }
2026
2027     if (!(target = GetHSUser(hs, targetuser->handle_info))) {
2028         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", targetuser->nick, hs->helpserv->nick);
2029         return 0;
2030     }
2031
2032     if ((hs->persist_lengths[PERSIST_T_HELPER] == PERSIST_PART) && !GetUserMode(hs->helpchan, user) && (hs_user->level < HlManager)) {
2033         helpserv_notice(user, "HSMSG_REQ_HIM_NOT_IN_HELPCHAN", targetuser->nick, hs->helpchan->name);
2034         return 0;
2035     }
2036
2037     helpserv_assign(from_opserv, hs, targetuser, user, req);
2038     return 1;
2039 }
2040
2041 static HELPSERV_FUNC(cmd_addnote) {
2042     char text[MAX_LINE_SIZE], timestr[MAX_LINE_SIZE], *note;
2043     struct helpserv_request *req;
2044     struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
2045     int num_requests=0;
2046     time_t feh;
2047
2048     REQUIRE_PARMS(3);
2049
2050     if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
2051         helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
2052         return 0;
2053     }
2054
2055     if (num_requests > 1)
2056         helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
2057
2058     note = unsplit_string(argv+2, argc-2, NULL);
2059
2060     feh = now;
2061     strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&feh));
2062     snprintf(text, MAX_LINE_SIZE, "[Helper note at %s]:", timestr);
2063     string_list_append(req->text, strdup(text));
2064     snprintf(text, MAX_LINE_SIZE, "  <%s> %s", user->handle_info->handle, note);
2065     string_list_append(req->text, strdup(text));
2066
2067     helpserv_notice(user, "HSMSG_REQMSG_NOTE_ADDED", req->id);
2068
2069     return 1;
2070 }
2071
2072 static HELPSERV_FUNC(cmd_page) {
2073     REQUIRE_PARMS(2);
2074
2075     helpserv_page(PGSRC_COMMAND, "HSMSG_PAGE_REQUEST", user->nick, unsplit_string(argv+1, argc-1, NULL));
2076
2077     return 1;
2078 }
2079
2080 static HELPSERV_FUNC(cmd_stats) {
2081     struct helpserv_user *target, *hs_user;
2082     struct handle_info *target_handle;
2083     struct helpfile_table tbl;
2084     int i;
2085     char intervalstr[INTERVALLEN], buf[16];
2086
2087     hs_user = from_opserv ? NULL : GetHSUser(hs, user->handle_info);
2088
2089     if (argc > 1) {
2090         if (!from_opserv && (hs_user->level < HlManager)) {
2091             helpserv_notice(user, "HSMSG_NEED_MANAGER");
2092             return 0;
2093         }
2094
2095         if (!(target_handle = helpserv_get_handle_info(user, argv[1]))) {
2096             return 0;
2097         }
2098
2099         if (!(target = GetHSUser(hs, target_handle))) {
2100             helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", target_handle->handle, hs->helpserv->nick);
2101             return 0;
2102         }
2103     } else {
2104         if (from_opserv) {
2105             helpserv_notice(user, "HSMSG_OPSERV_NEED_USER");
2106             return 0;
2107         }
2108         target = hs_user;
2109     }
2110
2111     helpserv_notice(user, "HSMSG_STATS_TOP", hs->helpserv->nick, target->handle->handle, weekday_names[target->week_start]);
2112
2113     tbl.length = 6;
2114     tbl.width = 2;
2115     tbl.flags = TABLE_NO_FREE;
2116     tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2117     tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2118     tbl.contents[0][0] = "";
2119     tbl.contents[0][1] = "Recorded time";
2120     for (i=0; i < 5; i++) {
2121         unsigned int week_time = target->time_per_week[i];
2122         tbl.contents[i+1] = alloca(tbl.width * sizeof(**tbl.contents));
2123         if ((i == 0 || i == 4) && target->join_time)
2124             week_time += now - target->join_time;
2125         helpserv_interval(intervalstr, week_time);
2126         tbl.contents[i+1][1] = strdup(intervalstr);
2127     }
2128     tbl.contents[1][0] = "This week";
2129     tbl.contents[2][0] = "Last week";
2130     tbl.contents[3][0] = "2 weeks ago";
2131     tbl.contents[4][0] = "3 weeks ago";
2132     tbl.contents[5][0] = "Total";
2133
2134     helpserv_notice(user, "HSMSG_STATS_TIME", hs->helpchan->name);
2135     table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
2136
2137     for (i=1; i <= 5; i++)
2138         free((char *)tbl.contents[i][1]);
2139
2140     tbl.length = 5;
2141     tbl.width = 4;
2142     tbl.flags = TABLE_NO_FREE;
2143     tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2144     tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2145     tbl.contents[1] = alloca(tbl.width * sizeof(**tbl.contents));
2146     tbl.contents[2] = alloca(tbl.width * sizeof(**tbl.contents));
2147     tbl.contents[3] = alloca(tbl.width * sizeof(**tbl.contents));
2148     tbl.contents[4] = alloca(tbl.width * sizeof(**tbl.contents));
2149     tbl.contents[0][0] = "Category";
2150     tbl.contents[0][1] = "This week";
2151     tbl.contents[0][2] = "Last week";
2152     tbl.contents[0][3] = "Total";
2153
2154     tbl.contents[1][0] = "Requests picked up";
2155     for (i=0; i < 3; i++) {
2156         sprintf(buf, "%u", target->picked_up[(i == 2 ? 4 : i)]);
2157         tbl.contents[1][i+1] = strdup(buf);
2158     }
2159     tbl.contents[2][0] = "Requests closed";
2160     for (i=0; i < 3; i++) {
2161         sprintf(buf, "%u", target->closed[(i == 2 ? 4 : i)]);
2162         tbl.contents[2][i+1] = strdup(buf);
2163     }
2164     tbl.contents[3][0] = "Reassigned from";
2165     for (i=0; i < 3; i++) {
2166         sprintf(buf, "%u", target->reassigned_from[(i == 2 ? 4 : i)]);
2167         tbl.contents[3][i+1] = strdup(buf);
2168     }
2169     tbl.contents[4][0] = "Reassigned to";
2170     for (i=0; i < 3; i++) {
2171         sprintf(buf, "%u", target->reassigned_to[(i == 2 ? 4 : i)]);
2172         tbl.contents[4][i+1] = strdup(buf);
2173     }
2174
2175     helpserv_notice(user, "HSMSG_STATS_REQS");
2176     table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
2177
2178     for (i=1; i < 5; i++) {
2179         free((char *)tbl.contents[i][1]);
2180         free((char *)tbl.contents[i][2]);
2181         free((char *)tbl.contents[i][3]);
2182     }
2183
2184     return 1;
2185 }
2186
2187 static HELPSERV_FUNC(cmd_statsreport) {
2188     int use_privmsg=1;
2189     struct helpfile_table tbl;
2190     dict_iterator_t it;
2191     unsigned int line, i;
2192     struct userNode *srcbot = from_opserv ? opserv : hs->helpserv;
2193
2194     if ((argc > 1) && !irccasecmp(argv[1], "NOTICE"))
2195         use_privmsg = 0;
2196
2197     tbl.length = dict_size(hs->users)+1;
2198     tbl.width = 3;
2199     tbl.flags = TABLE_NO_FREE;
2200     tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2201     tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2202     tbl.contents[0][0] = "Account";
2203     tbl.contents[0][1] = "Requests";
2204     tbl.contents[0][2] = "Time helping";
2205
2206     for (it=dict_first(hs->users), line=0; it; it=iter_next(it)) {
2207         struct helpserv_user *hs_user=iter_data(it);
2208
2209         tbl.contents[++line] = alloca(tbl.width * sizeof(**tbl.contents));
2210         tbl.contents[line][0] = hs_user->handle->handle;
2211         tbl.contents[line][1] = malloc(12);
2212         tbl.contents[line][2] = malloc(32); /* A bit more than needed */
2213     }
2214
2215     /* 4 to 1 instead of 3 to 0 because it's unsigned */
2216     for (i=4; i > 0; i--) {
2217         for (it=dict_first(hs->users), line=0; it; it=iter_next(it)) {
2218             struct helpserv_user *hs_user = iter_data(it);
2219             /* Time */
2220             unsigned int week_time = hs_user->time_per_week[i-1];
2221             if ((i==1) && hs_user->join_time)
2222                 week_time += now - hs_user->join_time;
2223             helpserv_interval((char *)tbl.contents[++line][2], week_time);
2224
2225             /* Requests */
2226             sprintf((char *)tbl.contents[line][1], "%u", hs_user->picked_up[i-1]+hs_user->reassigned_to[i-1]);
2227         }
2228         send_target_message(use_privmsg, user->nick, srcbot, statsreport_week[i-1]);
2229         table_send(srcbot, user->nick, 0, (use_privmsg ? irc_privmsg : irc_notice), tbl);
2230     }
2231
2232     for (line=1; line <= dict_size(hs->users); line++) {
2233         free((char *)tbl.contents[line][1]);
2234         free((char *)tbl.contents[line][2]);
2235     }
2236
2237     return 1;
2238 }
2239
2240 static int
2241 helpserv_in_channel(struct helpserv_bot *hs, struct chanNode *channel) {
2242     enum page_source pgsrc;
2243     if (channel == hs->helpchan)
2244         return 1;
2245     for (pgsrc=0; pgsrc<PGSRC_COUNT; pgsrc++)
2246         if (channel == hs->page_targets[pgsrc])
2247             return 1;
2248     return 0;
2249 }
2250
2251 static HELPSERV_FUNC(cmd_move) {
2252     if (!hs) {
2253         helpserv_notice(user, "HSMSG_INVALID_BOT");
2254         return 0;
2255     }
2256
2257     REQUIRE_PARMS(2);
2258
2259     if (is_valid_nick(argv[1])) {
2260         char *newnick = argv[1], oldnick[NICKLEN], reason[MAXLEN];
2261
2262         strcpy(oldnick, hs->helpserv->nick);
2263
2264         if (GetUserH(newnick)) {
2265             helpserv_notice(user, "HSMSG_NICK_EXISTS", newnick);
2266             return 0;
2267         }
2268
2269         dict_remove2(helpserv_bots_dict, hs->helpserv->nick, 1);
2270         NickChange(hs->helpserv, newnick, 0);
2271         dict_insert(helpserv_bots_dict, hs->helpserv->nick, hs);
2272
2273         helpserv_notice(user, "HSMSG_RENAMED", oldnick, newnick);
2274
2275         snprintf(reason, MAXLEN, "HelpServ bot %s (in %s) renamed to %s by %s.", oldnick, hs->helpchan->name, newnick, user->nick);
2276         global_message(MESSAGE_RECIPIENT_OPERS, reason);
2277
2278         return 1;
2279     } else if (IsChannelName(argv[1])) {
2280         struct chanNode *old_helpchan = hs->helpchan;
2281         char *newchan = argv[1], oldchan[CHANNELLEN], reason[MAXLEN];
2282         struct helpserv_botlist *botlist;
2283
2284         strcpy(oldchan, hs->helpchan->name);
2285
2286         if (!irccasecmp(oldchan, newchan)) {
2287             helpserv_notice(user, "HSMSG_MOVE_SAME_CHANNEL", hs->helpserv->nick);
2288             return 0;
2289         }
2290
2291         if (opserv_bad_channel(newchan)) {
2292             helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", newchan);
2293             return 0;
2294         }
2295
2296         botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL);
2297         helpserv_botlist_remove(botlist, hs);
2298         if (botlist->used == 0) {
2299             dict_remove(helpserv_bots_bychan_dict, hs->helpchan->name);
2300         }
2301
2302         hs->helpchan = NULL;
2303         if (!helpserv_in_channel(hs, old_helpchan)) {
2304             snprintf(reason, MAXLEN, "Moved to %s by %s.", newchan, user->nick);
2305             DelChannelUser(hs->helpserv, old_helpchan, reason, 0);
2306         }
2307
2308         if (!(hs->helpchan = GetChannel(newchan))) {
2309             hs->helpchan = AddChannel(newchan, now, NULL, NULL);
2310             AddChannelUser(hs->helpserv, hs->helpchan)->modes |= MODE_CHANOP;
2311         } else if (!helpserv_in_channel(hs, old_helpchan)) {
2312             struct mod_chanmode change;
2313             mod_chanmode_init(&change);
2314             change.argc = 1;
2315             change.args[0].mode = MODE_CHANOP;
2316             change.args[0].u.member = AddChannelUser(hs->helpserv, hs->helpchan);
2317             mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
2318         }
2319
2320         if (!(botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL))) {
2321             botlist = helpserv_botlist_alloc();
2322             dict_insert(helpserv_bots_bychan_dict, hs->helpchan->name, botlist);
2323         }
2324         helpserv_botlist_append(botlist, hs);
2325
2326         snprintf(reason, MAXLEN, "HelpServ %s (%s) moved to %s by %s.", hs->helpserv->nick, oldchan, newchan, user->nick);
2327         global_message(MESSAGE_RECIPIENT_OPERS, reason);
2328
2329         return 1;
2330     } else {
2331         helpserv_notice(user, "HSMSG_INVALID_MOVE", argv[1]);
2332         return 0;
2333     }
2334 }
2335
2336 static HELPSERV_FUNC(cmd_bots) {
2337     dict_iterator_t it;
2338     struct helpfile_table tbl;
2339     unsigned int i;
2340
2341     helpserv_notice(user, "HSMSG_BOTLIST_HEADER");
2342
2343     tbl.length = dict_size(helpserv_bots_dict)+1;
2344     tbl.width = 4;
2345     tbl.flags = TABLE_NO_FREE;
2346     tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2347     tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2348     tbl.contents[0][0] = "Bot";
2349     tbl.contents[0][1] = "Channel";
2350     tbl.contents[0][2] = "Owner";
2351     tbl.contents[0][3] = "Inactivity";
2352
2353     for (it=dict_first(helpserv_bots_dict), i=1; it; it=iter_next(it), i++) {
2354         dict_iterator_t it2;
2355         struct helpserv_bot *bot;
2356         struct helpserv_user *owner=NULL;
2357
2358         bot = iter_data(it);
2359
2360         for (it2=dict_first(bot->users); it2; it2=iter_next(it2)) {
2361             if (((struct helpserv_user *)iter_data(it2))->level == HlOwner) {
2362                 owner = iter_data(it2);
2363                 break;
2364             }
2365         }
2366
2367         tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
2368         tbl.contents[i][0] = iter_key(it);
2369         tbl.contents[i][1] = bot->helpchan->name;
2370         tbl.contents[i][2] = owner ? owner->handle->handle : "None";
2371         tbl.contents[i][3] = alloca(INTERVALLEN);
2372         intervalString((char*)tbl.contents[i][3], now - bot->last_active, user->handle_info);
2373     }
2374
2375     table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
2376
2377     return 1;
2378 }
2379
2380 static void helpserv_page_helper_gone(struct helpserv_bot *hs, struct helpserv_request *req, const char *reason) {
2381     const int from_opserv = 0;
2382
2383     if (!req->helper)
2384         return;
2385
2386     /* Let the user know that their request is now unhandled */
2387     if (req->user) {
2388         struct modeNode *mn = GetUserMode(hs->helpchan, req->user);
2389         helpserv_msguser(req->user, "HSMSG_REQ_UNASSIGNED", req->id, reason);
2390         if (hs->auto_devoice && mn && (mn->modes & MODE_VOICE)) {
2391             struct mod_chanmode change;
2392             mod_chanmode_init(&change);
2393             change.argc = 1;
2394             change.args[0].mode = MODE_REMOVE | MODE_VOICE;
2395             change.args[0].u.member = mn;
2396             mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
2397         }
2398         if(req->handle)
2399             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_1", req->id, req->user->nick, req->handle->handle, req->helper->handle->handle, reason);
2400         else
2401             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_2", req->id, req->user->nick, req->helper->handle->handle, reason);
2402     } else {
2403         if(req->handle)
2404             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_3", req->id, req->handle->handle, req->helper->handle->handle, reason);
2405         else
2406             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_2", req->id, req->helper->handle->handle, reason);
2407     }
2408
2409     /* Now put it back in the queue */
2410     if (hs->unhandled == NULL) {
2411         /* Nothing there, put it at the front */
2412         hs->unhandled = req;
2413         req->next_unhandled = NULL;
2414     } else {
2415         /* Should it be at the front? */
2416         if (hs->unhandled->opened >= req->opened) {
2417             req->next_unhandled = hs->unhandled;
2418             hs->unhandled = req;
2419         } else {
2420             struct helpserv_request *unhandled;
2421             /* Find the request that this should be inserted AFTER */
2422             for (unhandled=hs->unhandled; unhandled->next_unhandled && (unhandled->next_unhandled->opened < req->opened); unhandled = unhandled->next_unhandled);
2423             req->next_unhandled = unhandled->next_unhandled;
2424             unhandled->next_unhandled = req;
2425         }
2426     }
2427
2428     req->helper = NULL;
2429 }
2430
2431 /* This takes care of WHINE_DELAY and IDLE_DELAY */
2432 static void run_whine_interval(void *data) {
2433     struct helpserv_bot *hs=data;
2434     struct helpfile_table tbl;
2435     unsigned int i;
2436
2437     /* First, run the WHINE_DELAY */
2438     if (hs->intervals[INTERVAL_WHINE_DELAY]
2439         && (hs->page_types[PGSRC_ALERT] != PAGE_NONE)
2440         && (hs->page_targets[PGSRC_ALERT] != NULL)
2441         && (!hs->intervals[INTERVAL_EMPTY_INTERVAL] || !hs->helpchan_empty)) {
2442         struct helpserv_request *unh;
2443         struct helpserv_reqlist reqlist;
2444         unsigned int queuesize=0;
2445
2446         helpserv_reqlist_init(&reqlist);
2447
2448         for (unh = hs->unhandled; unh; unh = unh->next_unhandled) {
2449             queuesize++;
2450             if ((now - unh->opened) >= hs->intervals[INTERVAL_WHINE_DELAY]) {
2451                 helpserv_reqlist_append(&reqlist, unh);
2452             }
2453         }
2454
2455         if (reqlist.used) {
2456             char strwhinedelay[INTERVALLEN];
2457
2458             intervalString(strwhinedelay, hs->intervals[INTERVAL_WHINE_DELAY], NULL);
2459 #if ANNOYING_ALERT_PAGES
2460             tbl.length = reqlist.used + 1;
2461             tbl.width = 4;
2462             tbl.flags = TABLE_NO_FREE;
2463             tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2464             tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2465             tbl.contents[0][0] = "ID#";
2466             tbl.contents[0][1] = "Nick";
2467             tbl.contents[0][2] = "Account";
2468             tbl.contents[0][3] = "Waiting time";
2469
2470             for (i=1; i <= reqlist.used; i++) {
2471                 char reqid[12], unh_time[INTERVALLEN];
2472                 unh = reqlist.list[i-1];
2473
2474                 tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
2475                 sprintf(reqid, "%lu", unh->id);
2476                 tbl.contents[i][0] = strdup(reqid);
2477                 tbl.contents[i][1] = unh->user ? unh->user->nick : "Not online";
2478                 tbl.contents[i][2] = unh->handle ? unh->handle->handle : "Not authed";
2479                 intervalString(unh_time, now - unh->opened, NULL);
2480                 tbl.contents[i][3] = strdup(unh_time);
2481             }
2482
2483             helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_WHINE_HEADER", reqlist.used, strwhinedelay, queuesize, dict_size(hs->requests));
2484             table_send(hs->helpserv, hs->page_targets[PGSRC_ALERT]->name, 0, page_type_funcs[hs->page_types[PGSRC_ALERT]], tbl);
2485
2486             for (i=1; i <= reqlist.used; i++) {
2487                 free((char *)tbl.contents[i][0]);
2488                 free((char *)tbl.contents[i][3]);
2489             }
2490 #else
2491             helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_WHINE_HEADER", reqlist.used, strwhinedelay, queuesize, dict_size(hs->requests));
2492 #endif
2493         }
2494
2495         helpserv_reqlist_clean(&reqlist);
2496     }
2497
2498     /* Next run IDLE_DELAY */
2499     if (hs->intervals[INTERVAL_IDLE_DELAY]
2500         && (hs->page_types[PGSRC_STATUS] != PAGE_NONE)
2501         && (hs->page_targets[PGSRC_STATUS] != NULL)) {
2502         struct modeList mode_list;
2503
2504         modeList_init(&mode_list);
2505
2506         for (i=0; i < hs->helpchan->members.used; i++) {
2507             struct modeNode *mn = hs->helpchan->members.list[i];
2508             /* Ignore ops. Perhaps this should be a set option? */
2509             if (mn->modes & MODE_CHANOP)
2510                 continue;
2511             /* Check if they've been idle long enough */
2512             if ((unsigned)(now - mn->idle_since) < hs->intervals[INTERVAL_IDLE_DELAY])
2513                 continue;
2514             /* Add them to the list of idle people.. */
2515             modeList_append(&mode_list, mn);
2516         }
2517
2518         if (mode_list.used) {
2519             char stridledelay[INTERVALLEN];
2520
2521             tbl.length = mode_list.used + 1;
2522             tbl.width = 4;
2523             tbl.flags = TABLE_NO_FREE;
2524             tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
2525             tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
2526             tbl.contents[0][0] = "Nick";
2527             tbl.contents[0][1] = "Account";
2528             tbl.contents[0][2] = "ID#";
2529             tbl.contents[0][3] = "Idle time";
2530
2531             for (i=1; i <= mode_list.used; i++) {
2532                 char reqid[12], idle_time[INTERVALLEN];
2533                 struct helpserv_reqlist *reqlist;
2534                 struct modeNode *mn = mode_list.list[i-1];
2535
2536                 tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
2537                 tbl.contents[i][0] = mn->user->nick;
2538                 tbl.contents[i][1] = mn->user->handle_info ? mn->user->handle_info->handle : "Not authed";
2539
2540                 if ((reqlist = dict_find(helpserv_reqs_bynick_dict, mn->user->nick, NULL))) {
2541                     int j;
2542
2543                     for (j = reqlist->used-1; j >= 0; j--) {
2544                         struct helpserv_request *req = reqlist->list[j];
2545
2546                         if (req->hs == hs) {
2547                             sprintf(reqid, "%lu", req->id);
2548                             break;
2549                         }
2550                     }
2551
2552                     if (j < 0)
2553                         strcpy(reqid, "None");
2554                 } else {
2555                     strcpy(reqid, "None");
2556                 }
2557                 tbl.contents[i][2] = strdup(reqid);
2558
2559                 intervalString(idle_time, now - mn->idle_since, NULL);
2560                 tbl.contents[i][3] = strdup(idle_time);
2561             }
2562
2563             intervalString(stridledelay, hs->intervals[INTERVAL_IDLE_DELAY], NULL);
2564             helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_IDLE_HEADER", mode_list.used, hs->helpchan->name, stridledelay);
2565             table_send(hs->helpserv, hs->page_targets[PGSRC_STATUS]->name, 0, page_types[hs->page_types[PGSRC_STATUS]].func, tbl);
2566
2567             for (i=1; i <= mode_list.used; i++) {
2568                 free((char *)tbl.contents[i][2]);
2569                 free((char *)tbl.contents[i][3]);
2570             }
2571         }
2572
2573         modeList_clean(&mode_list);
2574     }
2575
2576     if (hs->intervals[INTERVAL_WHINE_INTERVAL]) {
2577         timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
2578     }
2579 }
2580
2581 /* Returns -1 if there's any helpers,
2582  * 0 if there are no helpers
2583  * >1 if there are trials (number of trials)
2584  */
2585 static int find_helpchan_helpers(struct helpserv_bot *hs) {
2586     int num_trials=0;
2587     dict_iterator_t it;
2588
2589     for (it=dict_first(hs->users); it; it=iter_next(it)) {
2590         struct helpserv_user *hs_user=iter_data(it);
2591
2592         if (find_handle_in_channel(hs->helpchan, hs_user->handle, NULL)) {
2593             if (hs_user->level >= HlHelper) {
2594                 hs->helpchan_empty = 0;
2595                 return -1;
2596             }
2597             num_trials++;
2598         }
2599     }
2600
2601     hs->helpchan_empty = 1;
2602     return num_trials;
2603 }
2604
2605
2606 static void run_empty_interval(void *data) {
2607     struct helpserv_bot *hs=data;
2608     int num_trials=find_helpchan_helpers(hs);
2609     unsigned int num_unh;
2610     struct helpserv_request *unh;
2611
2612     if (num_trials == -1)
2613         return;
2614     if (hs->req_on_join && !hs->unhandled)
2615         return;
2616
2617     for (num_unh=0, unh=hs->unhandled; unh; num_unh++)
2618         unh = unh->next_unhandled;
2619
2620     if (num_trials)
2621         helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_ONLYTRIALALERT", hs->helpchan->name, num_trials, num_unh);
2622     else
2623         helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_EMPTYALERT", hs->helpchan->name, num_unh);
2624
2625     if (hs->intervals[INTERVAL_EMPTY_INTERVAL])
2626         timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
2627 }
2628
2629 static void free_user(void *data) {
2630     struct helpserv_user *hs_user = data;
2631     struct helpserv_bot *hs = hs_user->hs;
2632     struct helpserv_userlist *userlist;
2633     dict_iterator_t it;
2634
2635     if (hs->requests) {
2636         for (it=dict_first(hs->requests); it; it=iter_next(it)) {
2637             struct helpserv_request *req = iter_data(it);
2638
2639             if (req->helper == hs_user)
2640                 helpserv_page_helper_gone(hs, req, "been deleted");
2641         }
2642     }
2643
2644     userlist = dict_find(helpserv_users_byhand_dict, hs_user->handle->handle, NULL);
2645     if (userlist->used == 1) {
2646         dict_remove(helpserv_users_byhand_dict, hs_user->handle->handle);
2647     } else {
2648         helpserv_userlist_remove(userlist, hs_user);
2649     }
2650
2651     free(data);
2652 }
2653
2654 static struct helpserv_bot *register_helpserv(const char *nick, const char *help_channel, const char *registrar) {
2655     struct helpserv_bot *hs;
2656     struct helpserv_botlist *botlist;
2657
2658     /* Laziness dictates calloc, since there's a lot to set to NULL or 0, and
2659      * it's a harmless default */
2660     hs = calloc(1, sizeof(struct helpserv_bot));
2661
2662     if (!(hs->helpserv = AddLocalUser(nick, nick, NULL, helpserv_conf.description, NULL))) {
2663         free(hs);
2664         return NULL;
2665     }
2666
2667     reg_privmsg_func(hs->helpserv, helpserv_botmsg);
2668
2669     if (!(hs->helpchan = GetChannel(help_channel))) {
2670         hs->helpchan = AddChannel(help_channel, now, NULL, NULL);
2671         AddChannelUser(hs->helpserv, hs->helpchan)->modes |= MODE_CHANOP;
2672     } else {
2673         struct mod_chanmode change;
2674         mod_chanmode_init(&change);
2675         change.argc = 1;
2676         change.args[0].mode = MODE_CHANOP;
2677         change.args[0].u.member = AddChannelUser(hs->helpserv, hs->helpchan);
2678         mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
2679     }
2680
2681     if (registrar)
2682         hs->registrar = strdup(registrar);
2683
2684     hs->users = dict_new();
2685     /* Don't free keys - they use the handle_info's handle field */
2686     dict_set_free_data(hs->users, free_user);
2687     hs->requests = dict_new();
2688     dict_set_free_keys(hs->requests, free);
2689     dict_set_free_data(hs->requests, free_request);
2690
2691     dict_insert(helpserv_bots_dict, hs->helpserv->nick, hs);
2692
2693     if (!(botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL))) {
2694         botlist = helpserv_botlist_alloc();
2695         dict_insert(helpserv_bots_bychan_dict, hs->helpchan->name, botlist);
2696     }
2697     helpserv_botlist_append(botlist, hs);
2698
2699     return hs;
2700 }
2701
2702 static HELPSERV_FUNC(cmd_register) {
2703     char *nick, *helpchan, reason[MAXLEN];
2704     struct handle_info *handle;
2705
2706     REQUIRE_PARMS(4);
2707     nick = argv[1];
2708     if (!is_valid_nick(nick)) {
2709         helpserv_notice(user, "HSMSG_ILLEGAL_NICK", nick);
2710         return 0;
2711     }
2712     if (GetUserH(nick)) {
2713         helpserv_notice(user, "HSMSG_NICK_EXISTS", nick);
2714         return 0;
2715     }
2716     helpchan = argv[2];
2717     if (!IsChannelName(helpchan)) {
2718         helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", helpchan);
2719         HELPSERV_SYNTAX();
2720         return 0;
2721     }
2722     if (opserv_bad_channel(helpchan)) {
2723         helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", helpchan);
2724         return 0;
2725     }
2726     if (!(handle = helpserv_get_handle_info(user, argv[3])))
2727         return 0;
2728
2729     if (!(hs = register_helpserv(nick, helpchan, user->handle_info->handle))) {
2730         helpserv_notice(user, "HSMSG_ERROR_ADDING_SERVICE", nick);
2731         return 0;
2732     }
2733
2734     hs->registered = now;
2735     helpserv_add_user(hs, handle, HlOwner);
2736
2737     helpserv_notice(user, "HSMSG_REG_SUCCESS", handle->handle, nick);
2738
2739     snprintf(reason, MAXLEN, "HelpServ %s (%s) registered to %s by %s.", nick, hs->helpchan->name, handle->handle, user->nick);
2740     /* Not sent to helpers, since they can't register HelpServ */
2741     global_message(MESSAGE_RECIPIENT_OPERS, reason);
2742     return 1;
2743 }
2744
2745 static void unregister_helpserv(struct helpserv_bot *hs) {
2746     enum message_type msgtype;
2747
2748     timeq_del(0, NULL, hs, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_FUNC);
2749
2750     /* Requests before users so that it doesn't spam mentioning now-unhandled
2751      * requests because the users were deleted */
2752     dict_delete(hs->requests);
2753     hs->requests = NULL; /* so we don't try to look up requests in free_user() */
2754     dict_delete(hs->users);
2755     free(hs->registrar);
2756
2757     for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++)
2758         free(hs->messages[msgtype]);
2759 }
2760
2761 static void helpserv_free_bot(void *data) {
2762     unregister_helpserv(data);
2763     free(data);
2764 }
2765
2766 static void helpserv_unregister(struct helpserv_bot *bot, const char *quit_fmt, const char *global_fmt, const char *actor) {
2767     char reason[MAXLEN], channame[CHANNELLEN], botname[NICKLEN];
2768     struct helpserv_botlist *botlist;
2769     size_t len;
2770
2771     botlist = dict_find(helpserv_bots_bychan_dict, bot->helpchan->name, NULL);
2772     helpserv_botlist_remove(botlist, bot);
2773     if (!botlist->used)
2774         dict_remove(helpserv_bots_bychan_dict, bot->helpchan->name);
2775     len = strlen(bot->helpserv->nick) + 1;
2776     safestrncpy(botname, bot->helpserv->nick, len);
2777     len = strlen(bot->helpchan->name) + 1;
2778     safestrncpy(channame, bot->helpchan->name, len);
2779     snprintf(reason, sizeof(reason), quit_fmt, actor);
2780     DelUser(bot->helpserv, NULL, 1, reason);
2781     dict_remove(helpserv_bots_dict, botname);
2782     snprintf(reason, sizeof(reason), global_fmt, botname, channame, actor);
2783     global_message(MESSAGE_RECIPIENT_OPERS, reason);
2784 }
2785
2786 static HELPSERV_FUNC(cmd_unregister) {
2787     if (!from_opserv) {
2788         if (argc < 2 || strcmp(argv[1], "CONFIRM")) {
2789             helpserv_notice(user, "HSMSG_NEED_UNREG_CONFIRM");
2790             return 0;
2791         }
2792         log_audit(HS_LOG, LOG_COMMAND, user, hs->helpserv, hs->helpchan->name, 0, "unregister CONFIRM");
2793     }
2794
2795     helpserv_unregister(hs, "Unregistered by %s", "HelpServ %s (%s) unregistered by %s.", user->nick);
2796     return from_opserv;
2797 }
2798
2799 static HELPSERV_FUNC(cmd_expire) {
2800     struct helpserv_botlist victims;
2801     struct helpserv_bot *bot;
2802     dict_iterator_t it, next;
2803     unsigned int count = 0;
2804
2805     memset(&victims, 0, sizeof(victims));
2806     for (it = dict_first(helpserv_bots_dict); it; it = next) {
2807         bot = iter_data(it);
2808         next = iter_next(it);
2809         if ((unsigned int)(now - bot->last_active) < helpserv_conf.expire_age)
2810             continue;
2811         helpserv_unregister(bot, "Registration expired due to inactivity", "HelpServ %s (%s) expired at request of %s.", user->nick);
2812         count++;
2813     }
2814     helpserv_notice(user, "HSMSG_EXPIRATION_DONE", count);
2815     return 1;
2816 }
2817
2818 static HELPSERV_FUNC(cmd_giveownership) {
2819     struct handle_info *hi;
2820     struct helpserv_user *new_owner, *old_owner, *hs_user;
2821     dict_iterator_t it;
2822     char reason[MAXLEN];
2823
2824     if (!from_opserv && ((argc < 3) || strcmp(argv[2], "CONFIRM"))) {
2825         helpserv_notice(user, "HSMSG_NEED_GIVEOWNERSHIP_CONFIRM");
2826         return 0;
2827     }
2828     hi = helpserv_get_handle_info(user, argv[1]);
2829     if (!hi)
2830         return 0;
2831     new_owner = GetHSUser(hs, hi);
2832     if (!new_owner) {
2833         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", hi->handle, hs->helpserv->nick);
2834         return 0;
2835     }
2836     if (!from_opserv)
2837         old_owner = GetHSUser(hs, user->handle_info);
2838     else for (it = dict_first(hs->users), old_owner = NULL; it; it = iter_next(it)) {
2839         hs_user = iter_data(it);
2840         if (hs_user->level != HlOwner)
2841             continue;
2842         if (old_owner) {
2843             helpserv_notice(user, "HSMSG_MULTIPLE_OWNERS", hs->helpserv->nick);
2844             return 0;
2845         }
2846         old_owner = hs_user;
2847     }
2848     if (!from_opserv && (new_owner->handle == user->handle_info)) {
2849         helpserv_notice(user, "HSMSG_NO_TRANSFER_SELF");
2850         return 0;
2851     }
2852     if (old_owner)
2853         old_owner->level = HlManager;
2854     new_owner->level = HlOwner;
2855     helpserv_notice(user, "HSMSG_OWNERSHIP_GIVEN", hs->helpserv->nick, new_owner->handle->handle);
2856     sprintf(reason, "%s (%s) ownership transferred to %s by %s.", hs->helpserv->nick, hs->helpchan->name, new_owner->handle->handle, user->handle_info->handle);
2857     return 1;
2858 }
2859
2860 static HELPSERV_FUNC(cmd_weekstart) {
2861     struct handle_info *hi;
2862     struct helpserv_user *actor, *victim;
2863     int changed = 0;
2864
2865     REQUIRE_PARMS(2);
2866     actor = from_opserv ? NULL : GetHSUser(hs, user->handle_info);
2867     if (!(hi = helpserv_get_handle_info(user, argv[1])))
2868         return 0;
2869     if (!(victim = GetHSUser(hs, hi))) {
2870         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", hi->handle, hs->helpserv->nick);
2871         return 0;
2872     }
2873     if (actor && (actor->level <= victim->level) && (actor != victim)) {
2874         helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
2875         return 0;
2876     }
2877     if (argc > 2 && (!actor || actor->level >= HlManager)) {
2878         int new_day = 7;
2879         switch (argv[2][0]) {
2880         case 's': case 'S':
2881             if ((argv[2][1] == 'u') || (argv[2][1] == 'U'))
2882                 new_day = 0;
2883             else if ((argv[2][1] == 'a') || (argv[2][1] == 'A'))
2884                 new_day = 6;
2885             break;
2886         case 'm': case 'M': new_day = 1; break;
2887         case 't': case 'T':
2888             if ((argv[2][1] == 'u') || (argv[2][1] == 'U'))
2889                 new_day = 2;
2890             else if ((argv[2][1] == 'h') || (argv[2][1] == 'H'))
2891                 new_day = 4;
2892             break;
2893         case 'w': case 'W': new_day = 3; break;
2894         case 'f': case 'F': new_day = 5; break;
2895         }
2896         if (new_day == 7) {
2897             helpserv_notice(user, "HSMSG_BAD_WEEKDAY", argv[2]);
2898             return 0;
2899         }
2900         victim->week_start = new_day;
2901         changed = 1;
2902     }
2903     helpserv_notice(user, "HSMSG_WEEK_STARTS", victim->handle->handle, weekday_names[victim->week_start]);
2904     return changed;
2905 }
2906
2907 static HELPSERV_FUNC(cmd_modstats) {
2908     struct handle_info *hi;
2909     struct helpserv_user *victim;
2910     const char *field_name;
2911     int week, mod;
2912     unsigned int *field = NULL;
2913     char *errptr;
2914
2915     REQUIRE_PARMS(5);
2916     if (!oper_has_access(user, (from_opserv ? opserv : hs->helpserv), helpserv_conf.modstats_level, 0))
2917         return 0;
2918     if (!(hi = helpserv_get_handle_info(user, argv[1])))
2919         return 0;
2920     if (!(victim = GetHSUser(hs, hi))) {
2921         helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", hi->handle, hs->helpserv->nick);
2922         return 0;
2923     }
2924
2925     field_name = argv[2];
2926     if (!strcasecmp(argv[3], "total"))
2927         week = 4;
2928     else if(!strcasecmp(argv[3], "current"))
2929         week = 0;
2930     else {
2931         week = strtoul(argv[3], &errptr, 0);
2932         if (*errptr != '\0') {
2933             helpserv_notice(user, "HSMSG_MODSTATS_BAD_WEEK");
2934             return 0;
2935         }
2936     }
2937     mod = strtol(argv[4], NULL, 0);
2938
2939     if (week < 0 || week > 4) {
2940         helpserv_notice(user, "HSMSG_MODSTATS_BAD_WEEK");
2941         return 0;
2942     }
2943
2944     if (!strcasecmp(field_name, "time")) {
2945         if (victim->join_time && (week == 0 || week == 4)) {
2946             victim->time_per_week[0] += now - victim->join_time;
2947             victim->time_per_week[4] += now - victim->join_time;
2948             victim->join_time = now;
2949         }
2950         field = victim->time_per_week;
2951     }
2952     else if (!strcasecmp(field_name, "picked") || !strcasecmp(field_name, "picked_up") || !strcasecmp(field_name, "reqs"))
2953         field = victim->picked_up;
2954     else if (!strcasecmp(field_name, "closed"))
2955         field = victim->closed;
2956     else if (!strcasecmp(field_name, "ra_from") || !strcasecmp(field_name, "reassigned_from"))
2957         field = victim->reassigned_from;
2958     else if (!strcasecmp(field_name, "ra_to") || !strcasecmp(field_name, "reassigned_to"))
2959         field = victim->reassigned_to;
2960     else {
2961         helpserv_notice(user, "HSMSG_MODSTATS_BAD_FIELD");
2962         return 0;
2963     }
2964
2965     if (mod < 0 && mod < -(int)field[week]) {
2966         helpserv_notice(user, "HSMSG_MODSTATS_NEGATIVE");
2967         return 0;
2968     }
2969
2970     field[week] += mod;
2971     helpserv_notice(user, "HSMSG_MODSTATS_SUCCESS", victim->handle->handle);
2972
2973     return (mod != 0);
2974 }
2975
2976 static void set_page_target(struct helpserv_bot *hs, enum page_source idx, const char *target) {
2977     struct chanNode *new_target, *old_target;
2978
2979     if (target) {
2980         if (!IsChannelName(target)) {
2981             log_module(HS_LOG, LOG_ERROR, "%s has an invalid page target.", hs->helpserv->nick);
2982             return;
2983         }
2984         new_target = GetChannel(target);
2985         if (!new_target) {
2986             new_target = AddChannel(target, now, NULL, NULL);
2987             AddChannelUser(hs->helpserv, new_target);
2988         }
2989     } else {
2990         new_target = NULL;
2991     }
2992     if (new_target == hs->page_targets[idx])
2993         return;
2994     old_target = hs->page_targets[idx];
2995     hs->page_targets[idx] = NULL;
2996     if (old_target && !helpserv_in_channel(hs, old_target))
2997         DelChannelUser(hs->helpserv, old_target, "Changing page target.", 0);
2998     if (new_target && !helpserv_in_channel(hs, new_target)) {
2999         struct mod_chanmode change;
3000         mod_chanmode_init(&change);
3001         change.argc = 1;
3002         change.args[0].mode = MODE_CHANOP;
3003         change.args[0].u.member = AddChannelUser(hs->helpserv, new_target);
3004         mod_chanmode_announce(hs->helpserv, new_target, &change);
3005     }
3006     hs->page_targets[idx] = new_target;
3007 }
3008
3009 static int opt_page_target(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum page_source idx) {
3010     int changed = 0;
3011
3012     if (argc > 0) {
3013         if (!IsOper(user)) {
3014             helpserv_notice(user, "HSMSG_SET_NEED_OPER");
3015             return 0;
3016         }
3017         if (!strcmp(argv[0], "*")) {
3018             set_page_target(hs, idx, NULL);
3019             changed = 1;
3020         } else if (!IsChannelName(argv[0])) {
3021             helpserv_notice(user, "MSG_NOT_CHANNEL_NAME");
3022             return 0;
3023         } else {
3024             set_page_target(hs, idx, argv[0]);
3025             changed = 1;
3026         }
3027     }
3028     if (hs->page_targets[idx])
3029         helpserv_notice(user, page_sources[idx].print_target, hs->page_targets[idx]->name);
3030     else
3031         helpserv_notice(user, page_sources[idx].print_target, user_find_message(user, "MSG_NONE"));
3032     return changed;
3033 }
3034
3035 static HELPSERV_OPTION(opt_pagetarget_command) {
3036     return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_COMMAND);
3037 }
3038
3039 static HELPSERV_OPTION(opt_pagetarget_alert) {
3040     return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_ALERT);
3041 }
3042
3043 static HELPSERV_OPTION(opt_pagetarget_status) {
3044     return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_STATUS);
3045 }
3046
3047 static enum page_type page_type_from_name(const char *name) {
3048     enum page_type type;
3049     for (type=0; type<PAGE_COUNT; type++)
3050         if (!irccasecmp(page_types[type].db_name, name))
3051             return type;
3052     return PAGE_COUNT;
3053 }
3054
3055 static int opt_page_type(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum page_source idx) {
3056     enum page_type new_type;
3057     int changed=0;
3058
3059     if (argc > 0) {
3060         new_type = page_type_from_name(argv[0]);
3061         if (new_type == PAGE_COUNT) {
3062             helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
3063             return 0;
3064         }
3065         hs->page_types[idx] = new_type;
3066         changed = 1;
3067     }
3068     helpserv_notice(user, page_sources[idx].print_type,
3069                     user_find_message(user, page_types[hs->page_types[idx]].print_name));
3070     return changed;
3071 }
3072
3073 static HELPSERV_OPTION(opt_pagetype) {
3074     return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_COMMAND);
3075 }
3076
3077 static HELPSERV_OPTION(opt_alert_page_type) {
3078     return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_ALERT);
3079 }
3080
3081 static HELPSERV_OPTION(opt_status_page_type) {
3082     return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_STATUS);
3083 }
3084
3085 static int opt_message(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum message_type idx) {
3086     int changed=0;
3087
3088     if (argc > 0) {
3089         char *msg = unsplit_string(argv, argc, NULL);
3090         free(hs->messages[idx]);
3091         hs->messages[idx] = strcmp(msg, "*") ? strdup(msg) : NULL;
3092         changed = 1;
3093     }
3094     if (hs->messages[idx])
3095         helpserv_notice(user, message_types[idx].print_name, hs->messages[idx]);
3096     else
3097         helpserv_notice(user, message_types[idx].print_name, user_find_message(user, "MSG_NONE"));
3098     return changed;
3099 }
3100
3101 static HELPSERV_OPTION(opt_greeting) {
3102     return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_GREETING);
3103 }
3104
3105 static HELPSERV_OPTION(opt_req_opened) {
3106     return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_OPENED);
3107 }
3108
3109 static HELPSERV_OPTION(opt_req_assigned) {
3110     return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_ASSIGNED);
3111 }
3112
3113 static HELPSERV_OPTION(opt_req_closed) {
3114     return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_CLOSED);
3115 }
3116
3117 static int opt_interval(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum interval_type idx, unsigned int min) {
3118     char buf[INTERVALLEN];
3119     int changed=0;
3120
3121     if (argc > 0) {
3122         unsigned long new_int = ParseInterval(argv[0]);
3123         if (!new_int && strcmp(argv[0], "0")) {
3124             helpserv_notice(user, "MSG_INVALID_DURATION", argv[0]);
3125             return 0;
3126         }
3127         if (new_int && new_int < min) {
3128             intervalString(buf, min, user->handle_info);
3129             helpserv_notice(user, "HSMSG_INVALID_INTERVAL", user_find_message(user, interval_types[idx].print_name), buf);
3130             return 0;
3131         }
3132         hs->intervals[idx] = new_int;
3133         changed = 1;
3134     }
3135     if (hs->intervals[idx]) {
3136         intervalString(buf, hs->intervals[idx], user->handle_info);
3137         helpserv_notice(user, interval_types[idx].print_name, buf);
3138     } else
3139         helpserv_notice(user, interval_types[idx].print_name, user_find_message(user, "HSMSG_0_DISABLED"));
3140     return changed;
3141 }
3142
3143 static HELPSERV_OPTION(opt_idle_delay) {
3144     return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_IDLE_DELAY, 60);
3145 }
3146
3147 static HELPSERV_OPTION(opt_whine_delay) {
3148     return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_WHINE_DELAY, 60);
3149 }
3150
3151 static HELPSERV_OPTION(opt_whine_interval) {
3152     unsigned int old_val = hs->intervals[INTERVAL_WHINE_INTERVAL];
3153     int retval;
3154
3155     retval = opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_WHINE_INTERVAL, 60);
3156
3157     if (!old_val && hs->intervals[INTERVAL_WHINE_INTERVAL]) {
3158         timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
3159     } else if (old_val && !hs->intervals[INTERVAL_WHINE_INTERVAL]) {
3160         timeq_del(0, run_whine_interval, hs, TIMEQ_IGNORE_WHEN);
3161     }
3162
3163     return retval;
3164 }
3165
3166 static HELPSERV_OPTION(opt_empty_interval) {
3167     unsigned int old_val = hs->intervals[INTERVAL_EMPTY_INTERVAL];
3168     int retval;
3169
3170     retval = opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_EMPTY_INTERVAL, 60);
3171
3172     if (!old_val && hs->intervals[INTERVAL_EMPTY_INTERVAL]) {
3173         timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
3174     } else if (old_val && !hs->intervals[INTERVAL_EMPTY_INTERVAL]) {
3175         timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
3176     }
3177
3178     return retval;
3179 }
3180
3181 static HELPSERV_OPTION(opt_stale_delay) {
3182     return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_STALE_DELAY, 60);
3183 }
3184
3185 static enum persistence_length persistence_from_name(const char *name) {
3186     enum persistence_length pers;
3187     for (pers=0; pers<PERSIST_COUNT; pers++)
3188         if (!irccasecmp(name, persistence_lengths[pers].db_name))
3189             return pers;
3190     return PERSIST_COUNT;
3191 }
3192
3193 static int opt_persist(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum persistence_type idx) {
3194     int changed=0;
3195
3196     if (argc > 0) {
3197         enum persistence_length new_pers = persistence_from_name(argv[0]);
3198         if (new_pers == PERSIST_COUNT) {
3199             helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
3200             return 0;
3201         }
3202         hs->persist_lengths[idx] = new_pers;
3203         changed = 1;
3204     }
3205     helpserv_notice(user, persistence_lengths[idx].print_name,
3206                     user_find_message(user, persistence_lengths[hs->persist_lengths[idx]].print_name));
3207     return changed;
3208 }
3209
3210 static HELPSERV_OPTION(opt_request_persistence) {
3211     return opt_persist(user, hs, from_opserv, argc, argv, PERSIST_T_REQUEST);
3212 }
3213
3214 static HELPSERV_OPTION(opt_helper_persistence) {
3215     return opt_persist(user, hs, from_opserv, argc, argv, PERSIST_T_HELPER);
3216 }
3217
3218 static enum notification_type notification_from_name(const char *name) {
3219     enum notification_type notify;
3220     for (notify=0; notify<NOTIFY_COUNT; notify++)
3221         if (!irccasecmp(name, notification_types[notify].db_name))
3222             return notify;
3223     return NOTIFY_COUNT;
3224 }
3225
3226 static HELPSERV_OPTION(opt_notification) {
3227     int changed=0;
3228
3229     if (argc > 0) {
3230         enum notification_type new_notify = notification_from_name(argv[0]);
3231         if (new_notify == NOTIFY_COUNT) {
3232             helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
3233             return 0;
3234         }
3235         if (!from_opserv && (new_notify == NOTIFY_HANDLE)) {
3236             helpserv_notice(user, "HSMSG_SET_NEED_OPER");
3237             return 0;
3238         }
3239         hs->notify = new_notify;
3240         changed = 1;
3241     }
3242     helpserv_notice(user, "HSMSG_SET_NOTIFICATION", user_find_message(user, notification_types[hs->notify].print_name));
3243     return changed;
3244 }
3245
3246 #define OPTION_UINT(var, name) do { \
3247     int changed=0; \
3248     if (argc > 0) { \
3249         (var) = strtoul(argv[0], NULL, 0); \
3250         changed = 1; \
3251     } \
3252     helpserv_notice(user, name, (var)); \
3253     return changed; \
3254 } while (0);
3255
3256 static HELPSERV_OPTION(opt_id_wrap) {
3257     OPTION_UINT(hs->id_wrap, "HSMSG_SET_IDWRAP");
3258 }
3259
3260 static HELPSERV_OPTION(opt_req_maxlen) {
3261     OPTION_UINT(hs->req_maxlen, "HSMSG_SET_REQMAXLEN");
3262 }
3263
3264 #define OPTION_BINARY(var, name) do { \
3265     int changed=0; \
3266     if (argc > 0) { \
3267         if (enabled_string(argv[0])) { \
3268             (var) = 1; \
3269             changed = 1; \
3270         } else if (disabled_string(argv[0])) { \
3271             (var) = 0; \
3272             changed = 1; \
3273         } else { \
3274             helpserv_notice(user, "MSG_INVALID_BINARY", argv[0]); \
3275             return 0; \
3276         } \
3277     } \
3278     helpserv_notice(user, name, user_find_message(user, (var) ? "MSG_ON" : "MSG_OFF")); \
3279     return changed; \
3280 } while (0);
3281
3282 static HELPSERV_OPTION(opt_privmsg_only) {
3283     OPTION_BINARY(hs->privmsg_only, "HSMSG_SET_PRIVMSGONLY");
3284 }
3285
3286 static HELPSERV_OPTION(opt_req_on_join) {
3287     OPTION_BINARY(hs->req_on_join, "HSMSG_SET_REQONJOIN");
3288 }
3289
3290 static HELPSERV_OPTION(opt_auto_voice) {
3291     OPTION_BINARY(hs->auto_voice, "HSMSG_SET_AUTOVOICE");
3292 }
3293
3294 static HELPSERV_OPTION(opt_auto_devoice) {
3295     OPTION_BINARY(hs->auto_devoice, "HSMSG_SET_AUTODEVOICE");
3296 }
3297
3298 static HELPSERV_FUNC(cmd_set) {
3299     helpserv_option_func_t *opt;
3300
3301     if (argc < 2) {
3302         unsigned int i;
3303         helpserv_option_func_t *display[] = {
3304             opt_pagetarget_command, opt_pagetarget_alert, opt_pagetarget_status,
3305             opt_pagetype, opt_alert_page_type, opt_status_page_type,
3306             opt_greeting, opt_req_opened, opt_req_assigned, opt_req_closed,
3307             opt_idle_delay, opt_whine_delay, opt_whine_interval,
3308             opt_empty_interval, opt_stale_delay, opt_request_persistence,
3309             opt_helper_persistence, opt_notification, opt_id_wrap,
3310             opt_req_maxlen, opt_privmsg_only, opt_req_on_join, opt_auto_voice,
3311             opt_auto_devoice
3312         };
3313
3314         helpserv_notice(user, "HSMSG_QUEUE_OPTIONS");
3315         for (i=0; i<ArrayLength(display); i++)
3316             display[i](user, hs, from_opserv, 0, argv);
3317         return 1;
3318     }
3319
3320     if (!(opt = dict_find(helpserv_option_dict, argv[1], NULL))) {
3321         helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[1]);
3322         return 0;
3323     }
3324
3325     if ((argc > 2) && !from_opserv) {
3326         struct helpserv_user *hs_user;
3327
3328         if (!(hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
3329             helpserv_notice(user, "HSMSG_WTF_WHO_ARE_YOU", hs->helpserv->nick);
3330             return 0;
3331         }
3332
3333         if (hs_user->level < HlManager) {
3334             helpserv_notice(user, "HSMSG_NEED_MANAGER");
3335             return 0;
3336         }
3337     }
3338     return opt(user, hs, from_opserv, argc-2, argv+2);
3339 }
3340
3341 static int user_write_helper(const char *key, void *data, void *extra) {
3342     struct helpserv_user *hs_user = data;
3343     struct saxdb_context *ctx = extra;
3344     struct string_list strlist;
3345     char str[5][16], *strs[5];
3346     unsigned int i;
3347
3348     saxdb_start_record(ctx, key, 0);
3349     /* Helper identification. */
3350     saxdb_write_string(ctx, KEY_HELPER_LEVEL, helpserv_level2str(hs_user->level));
3351     saxdb_write_string(ctx, KEY_HELPER_HELPMODE, (hs_user->help_mode ? "1" : "0"));
3352     saxdb_write_int(ctx, KEY_HELPER_WEEKSTART, hs_user->week_start);
3353     /* Helper stats */
3354     saxdb_start_record(ctx, KEY_HELPER_STATS, 0);
3355     for (i=0; i < ArrayLength(strs); ++i)
3356         strs[i] = str[i];
3357     strlist.list = strs;
3358     strlist.used = 5;
3359     /* Time in help channel */
3360     for (i=0; i < strlist.used; i++) {
3361         unsigned int week_time = hs_user->time_per_week[i];
3362         if ((i==0 || i==4) && hs_user->join_time)
3363             week_time += now - hs_user->join_time;
3364         sprintf(str[i], "%u", week_time);
3365     }
3366     saxdb_write_string_list(ctx, KEY_HELPER_STATS_TIME, &strlist);
3367     /* Requests picked up */
3368     for (i=0; i < strlist.used; i++)
3369         sprintf(str[i], "%u", hs_user->picked_up[i]);
3370     saxdb_write_string_list(ctx, KEY_HELPER_STATS_PICKUP, &strlist);
3371     /* Requests closed */
3372     for (i=0; i < strlist.used; i++)
3373         sprintf(str[i], "%u", hs_user->closed[i]);
3374     saxdb_write_string_list(ctx, KEY_HELPER_STATS_CLOSE, &strlist);
3375     /* Requests reassigned from user */
3376     for (i=0; i < strlist.used; i++)
3377         sprintf(str[i], "%u", hs_user->reassigned_from[i]);
3378     saxdb_write_string_list(ctx, KEY_HELPER_STATS_REASSIGNFROM, &strlist);
3379     /* Requests reassigned to user */
3380     for (i=0; i < strlist.used; i++)
3381         sprintf(str[i], "%u", hs_user->reassigned_to[i]);
3382     saxdb_write_string_list(ctx, KEY_HELPER_STATS_REASSIGNTO, &strlist);
3383     /* End of stats and whole record. */
3384     saxdb_end_record(ctx);
3385     saxdb_end_record(ctx);
3386     return 0;
3387 }
3388
3389 static int user_read_helper(const char *key, void *data, void *extra) {
3390     struct record_data *rd = data;
3391     struct helpserv_bot *hs = extra;
3392     struct helpserv_user *hs_user;
3393     struct handle_info *handle;
3394     dict_t stats;
3395     enum helpserv_level level;
3396     char *str;
3397     struct string_list *strlist;
3398     unsigned int i;
3399
3400     if (rd->type != RECDB_OBJECT || !dict_size(rd->d.object)) {
3401         log_module(HS_LOG, LOG_ERROR, "Invalid user %s for %s.", key, hs->helpserv->nick);
3402         return 0;
3403     }
3404
3405     if (!(handle = get_handle_info(key))) {
3406         log_module(HS_LOG, LOG_ERROR, "Nonexistant account %s for %s.", key, hs->helpserv->nick);
3407         return 0;
3408     }
3409     str = database_get_data(rd->d.object, KEY_HELPER_LEVEL, RECDB_QSTRING);
3410     if (str) {
3411         level = helpserv_str2level(str);
3412         if (level == HlNone) {
3413             log_module(HS_LOG, LOG_ERROR, "Account %s has invalid level %s.", key, str);
3414             return 0;
3415         }
3416     } else {
3417         log_module(HS_LOG, LOG_ERROR, "Account %s has no level field for %s.", key, hs->helpserv->nick);
3418         return 0;
3419     }
3420
3421     hs_user = helpserv_add_user(hs, handle, level);
3422
3423     str = database_get_data(rd->d.object, KEY_HELPER_HELPMODE, RECDB_QSTRING);
3424     hs_user->help_mode = (str && strtol(str, NULL, 0)) ? 1 : 0;
3425     str = database_get_data(rd->d.object, KEY_HELPER_WEEKSTART, RECDB_QSTRING);
3426     hs_user->week_start = str ? strtol(str, NULL, 0) : 0;
3427
3428     /* Stats */
3429     stats = database_get_data(GET_RECORD_OBJECT(rd), KEY_HELPER_STATS, RECDB_OBJECT);
3430
3431     if (stats) {
3432         /* The tests for strlist->used are for converting the old format to the new one */
3433         strlist = database_get_data(stats, KEY_HELPER_STATS_TIME, RECDB_STRING_LIST);
3434         if (strlist) {
3435             for (i=0; i < 5 && i < strlist->used; i++)
3436                 hs_user->time_per_week[i] = strtoul(strlist->list[i], NULL, 0);
3437             if (strlist->used == 4)
3438                 hs_user->time_per_week[4] = hs_user->time_per_week[0]+hs_user->time_per_week[1]+hs_user->time_per_week[2]+hs_user->time_per_week[3];
3439         }
3440         strlist = database_get_data(stats, KEY_HELPER_STATS_PICKUP, RECDB_STRING_LIST);
3441         if (strlist) {
3442             for (i=0; i < 5 && i < strlist->used; i++)
3443                 hs_user->picked_up[i] = strtoul(strlist->list[i], NULL, 0);
3444             if (strlist->used == 2)
3445                 hs_user->picked_up[4] = hs_user->picked_up[0]+hs_user->picked_up[1];
3446         }
3447         strlist = database_get_data(stats, KEY_HELPER_STATS_CLOSE, RECDB_STRING_LIST);
3448         if (strlist) {
3449             for (i=0; i < 5 && i < strlist->used; i++)
3450                 hs_user->closed[i] = strtoul(strlist->list[i], NULL, 0);
3451             if (strlist->used == 2)
3452                 hs_user->closed[4] = hs_user->closed[0]+hs_user->closed[1];
3453         }
3454         strlist = database_get_data(stats, KEY_HELPER_STATS_REASSIGNFROM, RECDB_STRING_LIST);
3455         if (strlist) {
3456             for (i=0; i < 5 && i < strlist->used; i++)
3457                 hs_user->reassigned_from[i] = strtoul(strlist->list[i], NULL, 0);
3458             if (strlist->used == 2)
3459                 hs_user->reassigned_from[4] = hs_user->reassigned_from[0]+hs_user->reassigned_from[1];
3460         }
3461         strlist = database_get_data(stats, KEY_HELPER_STATS_REASSIGNTO, RECDB_STRING_LIST);
3462         if (strlist) {
3463             for (i=0; i < 5 && i < strlist->used; i++)
3464                 hs_user->reassigned_to[i] = strtoul(strlist->list[i], NULL, 0);
3465             if (strlist->used == 2)
3466                 hs_user->reassigned_to[4] = hs_user->reassigned_to[0]+hs_user->reassigned_to[1];
3467         }
3468     }
3469
3470     return 0;
3471 }
3472
3473 static int request_write_helper(const char *key, void *data, void *extra) {
3474     struct helpserv_request *request = data;
3475     struct saxdb_context *ctx = extra;
3476
3477     if (!request->handle)
3478         return 0;
3479
3480     saxdb_start_record(ctx, key, 0);
3481     if (request->helper) {
3482         saxdb_write_string(ctx, KEY_REQUEST_HELPER, request->helper->handle->handle);
3483         saxdb_write_int(ctx, KEY_REQUEST_ASSIGNED, request->assigned);
3484     }
3485     saxdb_write_string(ctx, KEY_REQUEST_HANDLE, request->handle->handle);
3486     saxdb_write_int(ctx, KEY_REQUEST_OPENED, request->opened);
3487     saxdb_write_string_list(ctx, KEY_REQUEST_TEXT, request->text);
3488     saxdb_end_record(ctx);
3489     return 0;
3490 }
3491
3492 static int request_read_helper(const char *key, void *data, void *extra) {
3493     struct record_data *rd = data;
3494     struct helpserv_bot *hs = extra;
3495     struct helpserv_request *request;
3496     struct string_list *strlist;
3497     char *str;
3498
3499     if (rd->type != RECDB_OBJECT || !dict_size(rd->d.object)) {
3500         log_module(HS_LOG, LOG_ERROR, "Invalid request %s:%s.", hs->helpserv->nick, key);
3501         return 0;
3502     }
3503
3504     request = calloc(1, sizeof(struct helpserv_request));
3505
3506     request->id = strtoul(key, NULL, 0);
3507     request->hs = hs;
3508     request->user = NULL;
3509     request->parent_nick_list = request->parent_hand_list = NULL;
3510
3511     str = database_get_data(rd->d.object, KEY_REQUEST_HANDLE, RECDB_QSTRING);
3512     if (!str || !(request->handle = get_handle_info(str))) {
3513         log_module(HS_LOG, LOG_ERROR, "Request %s:%s has an invalid or nonexistant account.", hs->helpserv->nick, key);
3514         free(request);
3515         return 0;
3516     }
3517     if (!(request->parent_hand_list = dict_find(helpserv_reqs_byhand_dict, request->handle->handle, NULL))) {
3518         request->parent_hand_list = helpserv_reqlist_alloc();
3519         dict_insert(helpserv_reqs_byhand_dict, request->handle->handle, request->parent_hand_list);
3520     }
3521     helpserv_reqlist_append(request->parent_hand_list, request);
3522
3523     str = database_get_data(rd->d.object, KEY_REQUEST_OPENED, RECDB_QSTRING);
3524     if (!str) {
3525         log_module(HS_LOG, LOG_ERROR, "Request %s:%s has a nonexistant opening time. Using time(NULL).", hs->helpserv->nick, key);
3526         request->opened = time(NULL);
3527     } else {
3528         request->opened = strtoul(str, NULL, 0);
3529     }
3530
3531     str = database_get_data(rd->d.object, KEY_REQUEST_ASSIGNED, RECDB_QSTRING);
3532     if (str)
3533         request->assigned = strtoul(str, NULL, 0);
3534
3535     str = database_get_data(rd->d.object, KEY_REQUEST_HELPER, RECDB_QSTRING);
3536     if (str) {
3537         if (!(request->helper = dict_find(hs->users, str, NULL))) {
3538             log_module(HS_LOG, LOG_ERROR, "Request %s:%s has an invalid or nonexistant helper.", hs->helpserv->nick, key);
3539             free(request);
3540             return 0;
3541         }
3542     } else {
3543         if (!hs->unhandled) {
3544             request->next_unhandled = NULL;
3545             hs->unhandled = request;
3546         } else if (hs->unhandled->opened > request->opened) {
3547             request->next_unhandled = hs->unhandled;
3548             hs->unhandled = request;
3549         } else {
3550             struct helpserv_request *unh;
3551             for (unh = hs->unhandled; unh->next_unhandled && (unh->next_unhandled->opened < request->opened); unh = unh->next_unhandled);
3552             request->next_unhandled = unh->next_unhandled;
3553             unh->next_unhandled = request;
3554         }
3555     }
3556
3557     strlist = database_get_data(rd->d.object, KEY_REQUEST_TEXT, RECDB_STRING_LIST);
3558     if (!strlist) {
3559         log_module(HS_LOG, LOG_ERROR, "Request %s:%s has no text.", hs->helpserv->nick, key);
3560         free(request);
3561         return 0;
3562     }
3563     request->text = string_list_copy(strlist);
3564
3565     dict_insert(hs->requests, strdup(key), request);
3566
3567     return 0;
3568 }
3569
3570 static int
3571 helpserv_bot_write(const char *key, void *data, void *extra) {
3572     const struct helpserv_bot *hs = data;
3573     struct saxdb_context *ctx = extra;
3574     enum page_source pagesrc;
3575     enum message_type msgtype;
3576     enum interval_type inttype;
3577     enum persistence_type persisttype;
3578     struct string_list *slist;
3579
3580     /* Entire bot */
3581     saxdb_start_record(ctx, key, 1);
3582
3583     /* Helper list */
3584     saxdb_start_record(ctx, KEY_HELPERS, 1);
3585     dict_foreach(hs->users, user_write_helper, ctx);
3586     saxdb_end_record(ctx);
3587
3588     /* Open requests */
3589     if (hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_CLOSE) {
3590         saxdb_start_record(ctx, KEY_REQUESTS, 0);
3591         dict_foreach(hs->requests, request_write_helper, ctx);
3592         saxdb_end_record(ctx);
3593     }
3594
3595     /* Other settings and state */
3596     saxdb_write_string(ctx, KEY_HELP_CHANNEL, hs->helpchan->name);
3597     slist = alloc_string_list(PGSRC_COUNT);
3598     for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
3599         struct chanNode *target = hs->page_targets[pagesrc];
3600         string_list_append(slist, strdup(target ? target->name : "*"));
3601     }
3602     saxdb_write_string_list(ctx, KEY_PAGE_DEST, slist);
3603     free_string_list(slist);
3604     for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
3605         const char *src = page_types[hs->page_types[pagesrc]].db_name;
3606         saxdb_write_string(ctx, page_sources[pagesrc].db_name, src);
3607     }
3608     for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++) {
3609         const char *msg = hs->messages[msgtype];
3610         if (msg)
3611             saxdb_write_string(ctx, message_types[msgtype].db_name, msg);
3612     }
3613     for (inttype=0; inttype<INTERVAL_COUNT; inttype++) {
3614         if (!hs->intervals[inttype])
3615             continue;
3616         saxdb_write_int(ctx, interval_types[inttype].db_name, hs->intervals[inttype]);
3617     }
3618     for (persisttype=0; persisttype<PERSIST_T_COUNT; persisttype++) {
3619         const char *persist = persistence_lengths[hs->persist_lengths[persisttype]].db_name;
3620         saxdb_write_string(ctx, persistence_types[persisttype].db_name, persist);
3621     }
3622     saxdb_write_string(ctx, KEY_NOTIFICATION, notification_types[hs->notify].db_name);
3623     saxdb_write_int(ctx, KEY_REGISTERED, hs->registered);
3624     saxdb_write_int(ctx, KEY_IDWRAP, hs->id_wrap);
3625     saxdb_write_int(ctx, KEY_REQ_MAXLEN, hs->req_maxlen);
3626     saxdb_write_int(ctx, KEY_LAST_REQUESTID, hs->last_requestid);
3627     if (hs->registrar)
3628         saxdb_write_string(ctx, KEY_REGISTRAR, hs->registrar);
3629     saxdb_write_int(ctx, KEY_PRIVMSG_ONLY, hs->privmsg_only);
3630     saxdb_write_int(ctx, KEY_REQ_ON_JOIN, hs->req_on_join);
3631     saxdb_write_int(ctx, KEY_AUTO_VOICE, hs->auto_voice);
3632     saxdb_write_int(ctx, KEY_AUTO_DEVOICE, hs->auto_devoice);
3633     saxdb_write_int(ctx, KEY_LAST_ACTIVE, hs->last_active);
3634
3635     /* End bot record */
3636     saxdb_end_record(ctx);
3637     return 0;
3638 }
3639
3640 static int
3641 helpserv_saxdb_write(struct saxdb_context *ctx) {
3642     saxdb_start_record(ctx, KEY_BOTS, 1);
3643     dict_foreach(helpserv_bots_dict, helpserv_bot_write, ctx);
3644     saxdb_end_record(ctx);
3645     saxdb_write_int(ctx, KEY_LAST_STATS_UPDATE, last_stats_update);
3646     return 0;
3647 }
3648
3649 static int helpserv_bot_read(const char *key, void *data, UNUSED_ARG(void *extra)) {
3650     struct record_data *br = data, *raw_record;
3651     struct helpserv_bot *hs;
3652     char *registrar, *helpchannel_name, *str;
3653     dict_t users, requests;
3654     enum page_source pagesrc;
3655     enum message_type msgtype;
3656     enum interval_type inttype;
3657     enum persistence_type persisttype;
3658
3659     users = database_get_data(GET_RECORD_OBJECT(br), KEY_HELPERS, RECDB_OBJECT);
3660     if (!users) {
3661         log_module(HS_LOG, LOG_ERROR, "%s has no users.", key);
3662         return 0;
3663     }
3664     helpchannel_name = database_get_data(GET_RECORD_OBJECT(br), KEY_HELP_CHANNEL, RECDB_QSTRING);
3665     if (!helpchannel_name || !IsChannelName(helpchannel_name)) {
3666         log_module(HS_LOG, LOG_ERROR, "%s has an invalid channel name.", key);
3667         return 0;
3668     }
3669     registrar = database_get_data(GET_RECORD_OBJECT(br), KEY_REGISTRAR, RECDB_QSTRING);
3670
3671     hs = register_helpserv(key, helpchannel_name, registrar);
3672
3673     raw_record = dict_find(GET_RECORD_OBJECT(br), KEY_PAGE_DEST, NULL);
3674     switch (raw_record ? raw_record->type : RECDB_INVALID) {
3675     case RECDB_QSTRING:
3676         set_page_target(hs, PGSRC_COMMAND, GET_RECORD_QSTRING(raw_record));
3677         pagesrc = PGSRC_COMMAND + 1;
3678         break;
3679     case RECDB_STRING_LIST: {
3680         struct string_list *slist = GET_RECORD_STRING_LIST(raw_record);
3681         for (pagesrc=0; (pagesrc<slist->used) && (pagesrc<PGSRC_COUNT); pagesrc++) {
3682             const char *dest = slist->list[pagesrc];
3683             set_page_target(hs, pagesrc, strcmp(dest, "*") ? dest : NULL);
3684         }
3685         break;
3686     }
3687     default:
3688         set_page_target(hs, PGSRC_COMMAND, NULL);
3689         pagesrc = PGSRC_COMMAND + 1;
3690         break;
3691     }
3692     while (pagesrc < PGSRC_COUNT) {
3693         set_page_target(hs, pagesrc++, hs->page_targets[PGSRC_COMMAND] ? hs->page_targets[PGSRC_COMMAND]->name : NULL);
3694     }
3695
3696     for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
3697         str = database_get_data(GET_RECORD_OBJECT(br), page_sources[pagesrc].db_name, RECDB_QSTRING);
3698         hs->page_types[pagesrc] = str ? page_type_from_name(str) : PAGE_NONE;
3699     }
3700
3701     for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++) {
3702         str = database_get_data(GET_RECORD_OBJECT(br), message_types[msgtype].db_name, RECDB_QSTRING);
3703         hs->messages[msgtype] = str ? strdup(str) : NULL;
3704     }
3705
3706     for (inttype=0; inttype<INTERVAL_COUNT; inttype++) {
3707         str = database_get_data(GET_RECORD_OBJECT(br), interval_types[inttype].db_name, RECDB_QSTRING);
3708         hs->intervals[inttype] = str ? ParseInterval(str) : 0;
3709     }
3710     if (hs->intervals[INTERVAL_WHINE_INTERVAL])
3711         timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
3712     if (hs->intervals[INTERVAL_EMPTY_INTERVAL])
3713         timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
3714
3715     for (persisttype=0; persisttype<PERSIST_T_COUNT; persisttype++) {
3716         str = database_get_data(GET_RECORD_OBJECT(br), persistence_types[persisttype].db_name, RECDB_QSTRING);
3717         hs->persist_lengths[persisttype] = str ? persistence_from_name(str) : PERSIST_QUIT;
3718     }
3719     str = database_get_data(GET_RECORD_OBJECT(br), KEY_NOTIFICATION, RECDB_QSTRING);
3720     hs->notify = str ? notification_from_name(str) : NOTIFY_NONE;
3721     str = database_get_data(GET_RECORD_OBJECT(br), KEY_REGISTERED, RECDB_QSTRING);
3722     if (str)
3723         hs->registered = strtol(str, NULL, 0);
3724     str = database_get_data(GET_RECORD_OBJECT(br), KEY_IDWRAP, RECDB_QSTRING);
3725     if (str)
3726         hs->id_wrap = strtoul(str, NULL, 0);
3727     str = database_get_data(GET_RECORD_OBJECT(br), KEY_REQ_MAXLEN, RECDB_QSTRING);
3728     if (str)
3729         hs->req_maxlen = strtoul(str, NULL, 0);
3730     str = database_get_data(GET_RECORD_OBJECT(br), KEY_LAST_REQUESTID, RECDB_QSTRING);
3731     if (str)
3732         hs->last_requestid = strtoul(str, NULL, 0);
3733     str = database_get_data(GET_RECORD_OBJECT(br), KEY_PRIVMSG_ONLY, RECDB_QSTRING);
3734     hs->privmsg_only = str ? enabled_string(str) : 0;
3735     str = database_get_data(GET_RECORD_OBJECT(br), KEY_REQ_ON_JOIN, RECDB_QSTRING);
3736     hs->req_on_join = str ? enabled_string(str) : 0;
3737     str = database_get_data(GET_RECORD_OBJECT(br), KEY_AUTO_VOICE, RECDB_QSTRING);
3738     hs->auto_voice = str ? enabled_string(str) : 0;
3739     str = database_get_data(GET_RECORD_OBJECT(br), KEY_AUTO_DEVOICE, RECDB_QSTRING);
3740     hs->auto_devoice = str ? enabled_string(str) : 0;
3741     str = database_get_data(GET_RECORD_OBJECT(br), KEY_LAST_ACTIVE, RECDB_QSTRING);
3742     hs->last_active = str ? strtoul(str, NULL, 0) : now;
3743
3744     dict_foreach(users, user_read_helper, hs);
3745
3746     requests = database_get_data(GET_RECORD_OBJECT(br), KEY_REQUESTS, RECDB_OBJECT);
3747     if (requests)
3748         dict_foreach(requests, request_read_helper, hs);
3749
3750     return 0;
3751 }
3752
3753 static int
3754 helpserv_saxdb_read(struct dict *conf_db) {
3755     dict_t object;
3756     char *str;
3757
3758     if ((object = database_get_data(conf_db, KEY_BOTS, RECDB_OBJECT))) {
3759         dict_foreach(object, helpserv_bot_read, NULL);
3760     }
3761
3762     str = database_get_data(conf_db, KEY_LAST_STATS_UPDATE, RECDB_QSTRING);
3763     last_stats_update = str ? strtoul(str, NULL, 0) : now;
3764     return 0;
3765 }
3766
3767 static void helpserv_conf_read(void) {
3768     dict_t conf_node;
3769     const char *str;
3770
3771     if (!(conf_node = conf_get_data(HELPSERV_CONF_NAME, RECDB_OBJECT))) {
3772         log_module(HS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type", HELPSERV_CONF_NAME);
3773         return;
3774     }
3775
3776     str = database_get_data(conf_node, "db_backup_freq", RECDB_QSTRING);
3777     helpserv_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
3778
3779     str = database_get_data(conf_node, "description", RECDB_QSTRING);
3780     helpserv_conf.description = str ? str : "Help Queue Manager";
3781
3782     str = database_get_data(conf_node, "reqlogfile", RECDB_QSTRING);
3783     if (str && strlen(str))
3784         helpserv_conf.reqlogfile = str;
3785     else
3786         helpserv_conf.reqlogfile = NULL;
3787
3788     str = database_get_data(conf_node, "expiration", RECDB_QSTRING);
3789     helpserv_conf.expire_age = ParseInterval(str ? str : "60d");
3790     str = database_get_data(conf_node, "modstats_level", RECDB_QSTRING);
3791     helpserv_conf.modstats_level = str ? strtoul(str, NULL, 0) : 850;
3792     str = database_get_data(conf_node, "user_escape", RECDB_QSTRING);
3793     helpserv_conf.user_escape = str ? str[0] : '@';
3794
3795     if (reqlog_f) {
3796         fclose(reqlog_f);
3797         reqlog_f = NULL;
3798     }
3799     if (helpserv_conf.reqlogfile
3800         && !(reqlog_f = fopen(helpserv_conf.reqlogfile, "a"))) {
3801         log_module(HS_LOG, LOG_ERROR, "Unable to open request logfile (%s): %s", helpserv_conf.reqlogfile, strerror(errno));
3802     }
3803 }
3804
3805 static struct helpserv_cmd *
3806 helpserv_define_func(const char *name, helpserv_func_t *func, enum helpserv_level level, long flags) {
3807     struct helpserv_cmd *cmd = calloc(1, sizeof(struct helpserv_cmd));
3808
3809     cmd->access = level;
3810     cmd->weight = 1.0;
3811     cmd->func = func;
3812     cmd->flags = flags;
3813     dict_insert(helpserv_func_dict, name, cmd);
3814
3815     return cmd;
3816 }
3817
3818 /* Drop requests that persist until part when a user leaves the chan */
3819 static void handle_part(struct modeNode *mn, UNUSED_ARG(const char *reason)) {
3820     struct helpserv_botlist *botlist;
3821     struct helpserv_userlist *userlist;
3822     const int from_opserv = 0; /* for helpserv_notice */
3823     unsigned int i;
3824
3825     if ((botlist = dict_find(helpserv_bots_bychan_dict, mn->channel->name, NULL))) {
3826         for (i=0; i < botlist->used; i++) {
3827             struct helpserv_bot *hs;
3828             dict_iterator_t it;
3829
3830             hs = botlist->list[i];
3831             if (!hs->helpserv)
3832                 continue;
3833             if (hs->persist_lengths[PERSIST_T_REQUEST] != PERSIST_PART)
3834                 continue;
3835
3836             for (it=dict_first(hs->requests); it; it=iter_next(it)) {
3837                 struct helpserv_request *req = iter_data(it);
3838
3839                 if (mn->user != req->user)
3840                     continue;
3841                 if (req->text->used) {
3842                     helpserv_message(hs, mn->user, MSGTYPE_REQ_DROPPED);
3843                     helpserv_msguser(mn->user, "HSMSG_REQ_DROPPED_PART", mn->channel->name, req->id);
3844                     if (req->helper && (hs->notify >= NOTIFY_DROP))
3845                         helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_PART", req->id, mn->user->nick);
3846                 }
3847                 helpserv_log_request(req, "Dropped");
3848                 dict_remove(hs->requests, iter_key(it));
3849                 break;
3850             }
3851         }
3852     }
3853
3854     if (mn->user->handle_info && (userlist = dict_find(helpserv_users_byhand_dict, mn->user->handle_info->handle, NULL))) {
3855         for (i=0; i < userlist->used; i++) {
3856             struct helpserv_user *hs_user = userlist->list[i];
3857             struct helpserv_bot *hs = hs_user->hs;
3858             dict_iterator_t it;
3859
3860             if ((hs->helpserv == NULL) || (hs->helpchan != mn->channel) || find_handle_in_channel(hs->helpchan, mn->user->handle_info, mn->user))
3861                 continue;
3862
3863             /* In case of the clock being set back for whatever reason,
3864              * minimize penalty. Don't duplicate this in handle_quit because
3865              * when users quit, handle_part is called for every channel first.
3866              */
3867             if (hs_user->join_time && (hs_user->join_time < now)) {
3868                 hs_user->time_per_week[0] += (unsigned int)(now - hs_user->join_time);
3869                 hs_user->time_per_week[4] += (unsigned int)(now - hs_user->join_time);
3870             }
3871             hs_user->join_time = 0;
3872
3873             for (it=dict_first(hs->requests); it; it=iter_next(it)) {
3874                 struct helpserv_request *req=iter_data(it);
3875
3876                 if ((hs->persist_lengths[PERSIST_T_HELPER] == PERSIST_PART)
3877                     && (req->helper == hs_user)) {
3878                     char our_reason[CHANNELLEN + 8];
3879                     sprintf(our_reason, "parted %s", mn->channel->name);
3880                     helpserv_page_helper_gone(hs, req, our_reason);
3881                 }
3882             }
3883
3884             if (hs->intervals[INTERVAL_EMPTY_INTERVAL] && hs_user->level >= HlHelper) {
3885                 int num_trials;
3886
3887                 if ((num_trials = find_helpchan_helpers(hs)) >= 0) {
3888                     unsigned int num_unh;
3889                     struct helpserv_request *unh;
3890
3891                     for (num_unh=0, unh=hs->unhandled; unh; num_unh++)
3892                         unh = unh->next_unhandled;
3893
3894                     if (num_trials) {
3895                         helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_FIRSTONLYTRIALALERT", hs->helpchan->name, mn->user->nick, num_trials, num_unh);
3896                     } else {
3897                         helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_FIRSTEMPTYALERT", hs->helpchan->name, mn->user->nick, num_unh);
3898                     }
3899                     if (num_unh || !hs->req_on_join) {
3900                         timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
3901                         timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
3902                     }
3903                 }
3904             }
3905         }
3906     }
3907 }
3908
3909 /* Drop requests that persist until part or quit when a user quits. Otherwise
3910  * set req->user to null (it's no longer valid) if they have a handle,
3911  * and drop it if they don't (nowhere to store the request).
3912  *
3913  * Unassign requests where req->helper persists until the helper parts or
3914  * quits. */
3915 static void handle_quit(struct userNode *user, UNUSED_ARG(struct userNode *killer), UNUSED_ARG(const char *why)) {
3916     struct helpserv_reqlist *reqlist;
3917     struct helpserv_userlist *userlist;
3918     unsigned int i, n;
3919
3920     if (IsLocal(user)) {
3921         struct helpserv_bot *hs;
3922         if ((hs = dict_find(helpserv_bots_dict, user->nick, NULL))) {
3923             hs->helpserv = NULL;
3924         }
3925         return;
3926     }
3927
3928     if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
3929         n = reqlist->used;
3930         for (i=0; i < n; i++) {
3931             struct helpserv_request *req = reqlist->list[0];
3932
3933             if ((req->hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_QUIT) || !req->handle) {
3934                 char buf[12];
3935                 sprintf(buf, "%lu", req->id);
3936
3937                 if (req->helper && (req->hs->notify >= NOTIFY_DROP))
3938                     helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_QUIT", req->id, req->user->nick);
3939
3940                 helpserv_log_request(req, "Dropped");
3941                 dict_remove(req->hs->requests, buf);
3942             } else {
3943                 req->user = NULL;
3944                 req->parent_nick_list = NULL;
3945                 helpserv_reqlist_remove(reqlist, req);
3946
3947                 if (req->helper && (req->hs->notify >= NOTIFY_USER))
3948                     helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_QUIT", req->id, user->nick);
3949             }
3950         }
3951
3952         dict_remove(helpserv_reqs_bynick_dict, user->nick);
3953     }
3954
3955     if (user->handle_info && (userlist = dict_find(helpserv_users_byhand_dict, user->handle_info->handle, NULL))) {
3956         for (i=0; i < userlist->used; i++) {
3957             struct helpserv_user *hs_user = userlist->list[i];
3958             struct helpserv_bot *hs = hs_user->hs;
3959             dict_iterator_t it;
3960
3961             if ((hs->helpserv == NULL) || user->next_authed || (user->handle_info->users != user))
3962                 continue;
3963
3964             for (it=dict_first(hs->requests); it; it=iter_next(it)) {
3965                 struct helpserv_request *req=iter_data(it);
3966
3967                 if ((hs->persist_lengths[PERSIST_T_HELPER] == PERSIST_QUIT) && (req->helper == hs_user)) {
3968                     helpserv_page_helper_gone(hs, req, "disconnected");
3969                 }
3970             }
3971         }
3972     }
3973 }
3974
3975 static void associate_requests_bybot(struct helpserv_bot *hs, struct userNode *user, int force_greet) {
3976     struct helpserv_reqlist *reqlist, *hand_reqlist=NULL;
3977     struct helpserv_request *newest=NULL, *nicknewest=NULL;
3978     unsigned int i;
3979     const int from_opserv = 0; /* For helpserv_notice */
3980
3981     if (!(user->handle_info && (hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) && !force_greet) {
3982         return;
3983     }
3984
3985     reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL);
3986
3987     if (hand_reqlist) {
3988         for (i=0; i < hand_reqlist->used; i++) {
3989             struct helpserv_request *req=hand_reqlist->list[i];
3990
3991             if (req->user || (req->hs != hs))
3992                 continue;
3993
3994             req->user = user;
3995             if (!reqlist) {
3996                 reqlist = helpserv_reqlist_alloc();
3997                 dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
3998             }
3999             req->parent_nick_list = reqlist;
4000             helpserv_reqlist_append(reqlist, req);
4001
4002             if (req->helper && (hs->notify >= NOTIFY_USER))
4003                 helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_FOUND", req->id, user->nick);
4004
4005             if (!newest || (newest->opened < req->opened))
4006                 newest = req;
4007         }
4008     }
4009
4010     /* If it's supposed to force a greeting, only bail out if there are no
4011      * requests at all. If it's not supposed to force a greeting, bail out if
4012      * nothing was changed. */
4013     if (!(newest || (force_greet && reqlist)))
4014         return;
4015
4016     /* Possible conditions here:
4017      * 1. newest == NULL, force_greeting == 1, reqlist != NULL
4018      * 2. newest != NULL, force_greeting doesn't matter, reqlist != NULL */
4019
4020     /* Figure out which request will get their next message */
4021     for (i=0; i < reqlist->used; i++) {
4022         struct helpserv_request *req=reqlist->list[i];
4023
4024         if (req->hs != hs)
4025             continue;
4026
4027         if (!nicknewest || (nicknewest->opened < req->opened))
4028             nicknewest = req;
4029
4030         if (hs->auto_voice && req->helper)
4031         {
4032             struct mod_chanmode change;
4033             mod_chanmode_init(&change);
4034             change.argc = 1;
4035             change.args[0].mode = MODE_VOICE;
4036             if ((change.args[0].u.member = GetUserMode(hs->helpchan, user)))
4037                 mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
4038         }
4039     }
4040
4041     if ((force_greet && nicknewest) || (newest && (nicknewest == newest))) {
4042         /* Let the user know. Either the user is forced to be greeted, or the
4043          * above has changed which request will get their next message. */
4044         helpserv_msguser(user, "HSMSG_GREET_EXISTING_REQ", hs->helpchan->name, nicknewest->id);
4045     }
4046 }
4047
4048 static void associate_requests_bychan(struct chanNode *chan, struct userNode *user, int force_greet) {
4049     struct helpserv_botlist *botlist;
4050     unsigned int i;
4051
4052     if (!(botlist = dict_find(helpserv_bots_bychan_dict, chan->name, NULL)))
4053         return;
4054
4055     for (i=0; i < botlist->used; i++)
4056         associate_requests_bybot(botlist->list[i], user, force_greet);
4057 }
4058
4059
4060 /* Greet users upon joining a helpserv channel (if greeting is set) and set
4061  * req->user to the user joining for all requests owned by the user's handle
4062  * (if any) with a req->user == NULL */
4063 static int handle_join(struct modeNode *mNode) {
4064     struct userNode *user = mNode->user;
4065     struct chanNode *chan = mNode->channel;
4066     struct helpserv_botlist *botlist;
4067     unsigned int i;
4068     const int from_opserv = 0; /* for helpserv_notice */
4069
4070     if (IsLocal(user))
4071         return 0;
4072
4073     if (!(botlist = dict_find(helpserv_bots_bychan_dict, chan->name, NULL)))
4074         return 0;
4075
4076     for (i=0; i < botlist->used; i++) {
4077         struct helpserv_bot *hs=botlist->list[i];
4078
4079         if (user->handle_info) {
4080             struct helpserv_user *hs_user;
4081
4082             if ((hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
4083                 if (!hs_user->join_time)
4084                     hs_user->join_time = now;
4085
4086                 if (hs_user->level >= HlHelper && hs->intervals[INTERVAL_EMPTY_INTERVAL] && hs->helpchan_empty) {
4087                     hs->helpchan_empty = 0;
4088                     timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
4089                     helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_EMPTYNOMORE", user->nick, hs->helpchan->name);
4090                 }
4091                 continue; /* Don't want helpers to have request-on-join */
4092             }
4093         }
4094
4095         if (self->burst && !hs->req_on_join)
4096             continue;
4097
4098         associate_requests_bybot(hs, user, 1);
4099
4100         helpserv_message(hs, user, MSGTYPE_GREETING);
4101
4102         /* Make sure this is at the end (because of the continues) */
4103         if (hs->req_on_join) {
4104             struct helpserv_reqlist *reqlist;
4105             unsigned int j;
4106
4107             if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
4108                 for (j=0; j < reqlist->used; j++)
4109                     if (reqlist->list[j]->hs == hs)
4110                         break;
4111                 if (j < reqlist->used)
4112                     continue;
4113             }
4114
4115             create_request(user, hs, 1);
4116         }
4117     }
4118     return 0;
4119 }
4120
4121 /* Update helpserv_reqs_bynick_dict upon nick change */
4122 static void handle_nickchange(struct userNode *user, const char *old_nick) {
4123     struct helpserv_reqlist *reqlist;
4124     unsigned int i;
4125
4126     if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, old_nick, NULL)))
4127         return;
4128
4129     /* Don't free the list when we switch it over to the new nick. */
4130     dict_remove2(helpserv_reqs_bynick_dict, old_nick, 1);
4131     dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
4132
4133     for (i=0; i < reqlist->used; i++) {
4134         struct helpserv_request *req=reqlist->list[i];
4135
4136         if (req->helper && (req->hs->notify >= NOTIFY_USER))
4137             helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_NICK", req->id, old_nick, user->nick);
4138     }
4139 }
4140
4141 /* Also update helpserv_reqs_byhand_dict upon handle rename */
4142 static void handle_nickserv_rename(struct handle_info *handle, const char *old_handle) {
4143     struct helpserv_reqlist *reqlist;
4144     struct helpserv_userlist *userlist;
4145     unsigned int i;
4146
4147     /* First, rename the handle in the requests dict */
4148     if ((reqlist = dict_find(helpserv_reqs_byhand_dict, old_handle, NULL))) {
4149         /* Don't free the list */
4150         dict_remove2(helpserv_reqs_byhand_dict, old_handle, 1);
4151         dict_insert(helpserv_reqs_byhand_dict, handle->handle, reqlist);
4152     }
4153
4154     /* Second, rename the handle in the users dict */
4155     if ((userlist = dict_find(helpserv_users_byhand_dict, old_handle, NULL))) {
4156         dict_remove2(helpserv_users_byhand_dict, old_handle, 1);
4157
4158         for (i=0; i < userlist->used; i++)
4159             dict_remove2(userlist->list[i]->hs->users, old_handle, 1);
4160
4161         dict_insert(helpserv_users_byhand_dict, handle->handle, userlist);
4162         for (i=0; i < userlist->used; i++)
4163             dict_insert(userlist->list[i]->hs->users, handle->handle, userlist->list[i]);
4164     }
4165
4166     if (reqlist) {
4167         for (i=0; i < reqlist->used; i++) {
4168             struct helpserv_request *req=reqlist->list[i];
4169
4170             if (req->helper && (req->hs->notify >= NOTIFY_HANDLE))
4171                 helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_RENAME", req->id, old_handle, handle->handle);
4172         }
4173     }
4174 }
4175
4176 /* Deals with two cases:
4177  * 1. No handle -> handle
4178  *    - Bots with a request assigned to both the user (w/o handle) and the
4179  *      handle can exist in this case. When a message is sent,
4180  *      helpserv_usermsg will append it to the most recently opened request.
4181  *    - Requests assigned to the handle are NOT assigned to the user, since
4182  *      multiple people can auth to the same handle at once. Wait for them to
4183  *      join / privmsg before setting req->user.
4184  * 2. Handle -> handle
4185  *    - Generally avoided, but sometimes the code may allow this.
4186  *    - Requests that persist only until part/quit are brought along to the
4187  *      new handle.
4188  *    - Requests that persist until closed (stay saved with the handle) are
4189  *      left with the old handle. This is to prevent the confusing situation
4190  *      where some requests are carried over to the new handle, and some are
4191  *      left (because req->handle is the same for all of them, but only some
4192  *      have req->user set).
4193  * - In either of the above cases, if a user is on a bot's userlist and has
4194  *   requests assigned to them, it will give them a list. */
4195 static void handle_nickserv_auth(struct userNode *user, struct handle_info *old_handle) {
4196     struct helpserv_reqlist *reqlist, *dellist=NULL, *hand_reqlist, *oldhand_reqlist;
4197     struct helpserv_userlist *userlist;
4198     unsigned int i, j;
4199     dict_iterator_t it;
4200     const int from_opserv = 0; /* for helpserv_notice */
4201
4202     if (!user->handle_info)
4203         return; /* Authed user is quitting */
4204
4205     if ((userlist = dict_find(helpserv_users_byhand_dict, user->handle_info->handle, NULL))) {
4206         for (i=0; i < userlist->used; i++) {
4207             struct helpserv_user *hs_user = userlist->list[i];
4208             struct helpserv_bot *hs = hs_user->hs;
4209             struct helpserv_reqlist helper_reqs;
4210             struct helpfile_table tbl;
4211
4212             if (!hs_user->join_time && find_handle_in_channel(hs->helpchan, hs_user->handle, NULL))
4213                 hs_user->join_time = now;
4214
4215             helpserv_reqlist_init(&helper_reqs);
4216
4217             for (it=dict_first(hs->requests); it; it=iter_next(it)) {
4218                 struct helpserv_request *req=iter_data(it);
4219
4220                 if (req->helper == hs_user)
4221                     helpserv_reqlist_append(&helper_reqs, req);
4222             }
4223
4224             if (helper_reqs.used) {
4225                 tbl.length = helper_reqs.used+1;
4226                 tbl.width = 5;
4227                 tbl.flags = TABLE_NO_FREE;
4228                 tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
4229                 tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
4230                 tbl.contents[0][0] = "Bot";
4231                 tbl.contents[0][1] = "ID#";
4232                 tbl.contents[0][2] = "Nick";
4233                 tbl.contents[0][3] = "Account";
4234                 tbl.contents[0][4] = "Opened";
4235
4236                 for (j=1; j <= helper_reqs.used; j++) {
4237                     struct helpserv_request *req=helper_reqs.list[j-1];
4238                     char reqid[12], timestr[MAX_LINE_SIZE];
4239                     time_t feh;
4240
4241                     tbl.contents[j] = alloca(tbl.width * sizeof(**tbl.contents));
4242                     tbl.contents[j][0] = req->hs->helpserv->nick;
4243                     sprintf(reqid, "%lu", req->id);
4244                     tbl.contents[j][1] = strdup(reqid);
4245                     tbl.contents[j][2] = req->user ? req->user->nick : "Not online";
4246                     tbl.contents[j][3] = req->handle ? req->handle->handle : "Not authed";
4247                     feh = req->opened;
4248                     strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&feh));
4249                     tbl.contents[j][4] = strdup(timestr);
4250                 }
4251
4252                 helpserv_notice(user, "HSMSG_REQLIST_AUTH");
4253                 table_send(hs->helpserv, user->nick, 0, NULL, tbl);
4254
4255                 for (j=1; j <= helper_reqs.used; j++) {
4256                     free((char *)tbl.contents[j][1]);
4257                     free((char *)tbl.contents[j][4]);
4258                 }
4259             }
4260
4261             helpserv_reqlist_clean(&helper_reqs);
4262         }
4263     }
4264
4265
4266     if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
4267         for (i=0; i < user->channels.used; i++)
4268             associate_requests_bychan(user->channels.list[i]->channel, user, 0);
4269         return;
4270     }
4271
4272     if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) {
4273         hand_reqlist = helpserv_reqlist_alloc();
4274         dict_insert(helpserv_reqs_byhand_dict, user->handle_info->handle, hand_reqlist);
4275     }
4276
4277     if (old_handle) {
4278         dellist = helpserv_reqlist_alloc();
4279         oldhand_reqlist = dict_find(helpserv_reqs_byhand_dict, old_handle->handle, NULL);
4280     } else {
4281         oldhand_reqlist = NULL;
4282     }
4283
4284     for (i=0; i < reqlist->used; i++) {
4285         struct helpserv_request *req = reqlist->list[i];
4286         struct helpserv_bot *hs=req->hs;
4287
4288         if (!old_handle || hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_PART || hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_QUIT) {
4289             /* The request needs to be assigned to the new handle; either it
4290              * only persists until part/quit (so it makes sense to keep it as
4291              * close to the user as possible, and if it's made persistent later
4292              * then it's attached to the new handle) or there is no old handle.
4293              */
4294
4295             req->handle = user->handle_info;
4296
4297             req->parent_hand_list = hand_reqlist;
4298             helpserv_reqlist_append(hand_reqlist, req);
4299
4300             if (oldhand_reqlist) {
4301                 if (oldhand_reqlist->used == 1) {
4302                     dict_remove(helpserv_reqs_byhand_dict, old_handle->handle);
4303                     oldhand_reqlist = NULL;
4304                 } else {
4305                     helpserv_reqlist_remove(oldhand_reqlist, req);
4306                 }
4307             }
4308
4309             if (old_handle) {
4310                 char buf[CHANNELLEN + 14];
4311
4312                 if (hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_PART) {
4313                     sprintf(buf, "part channel %s", hs->helpchan->name);
4314                 } else {
4315                     strcpy(buf, "quit irc");
4316                 }
4317
4318                 helpserv_msguser(user, "HSMSG_REQ_AUTH_MOVED", user->handle_info->handle, hs->helpchan->name, req->id, old_handle->handle, buf);
4319                 if (req->helper && (hs->notify >= NOTIFY_HANDLE))
4320                     helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_MOVE", req->id, user->handle_info->handle, old_handle->handle);
4321             } else {
4322                 if (req->helper && (hs->notify >= NOTIFY_HANDLE))
4323                     helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_AUTH", req->id, user->nick, user->handle_info->handle);
4324             }
4325         } else {
4326             req->user = NULL;
4327             req->parent_nick_list = NULL;
4328             /* Would rather not mess with the list while iterating through
4329              * it */
4330             helpserv_reqlist_append(dellist, req);
4331
4332             helpserv_msguser(user, "HSMSG_REQ_AUTH_STUCK", user->handle_info->handle, hs->helpchan->name, req->id, old_handle->handle);
4333             if (req->helper && (hs->notify >= NOTIFY_HANDLE))
4334                 helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_STUCK", req->id, user->nick, user->handle_info->handle, old_handle->handle);
4335         }
4336     }
4337
4338     if (old_handle) {
4339         if (dellist->used) {
4340             if (dellist->used == reqlist->used) {
4341                 dict_remove(helpserv_reqs_bynick_dict, user->nick);
4342             } else {
4343                 for (i=0; i < dellist->used; i++)
4344                     helpserv_reqlist_remove(reqlist, dellist->list[i]);
4345             }
4346         }
4347         helpserv_reqlist_free(dellist);
4348     }
4349
4350     for (i=0; i < user->channels.used; i++)
4351         associate_requests_bychan(user->channels.list[i]->channel, user, 0);
4352 }
4353
4354
4355 /* Disassociate all requests from the handle. If any have req->user == NULL
4356  * then give them to the user doing the unregistration (if not an oper/helper)
4357  * otherwise the first nick it finds authed (it lets them know about this). If
4358  * there are no users authed to the handle online, the requests are lost. This
4359  * may result in the user having >1 request/bot, and messages go to the most
4360  * recently opened request.
4361  *
4362  * Also, remove the user from all bots that it has access in.
4363  * helpserv_del_user() will take care of unassigning the requests. */
4364 static void handle_nickserv_unreg(struct userNode *user, struct handle_info *handle) {
4365     struct helpserv_reqlist *hand_reqlist;
4366     struct helpserv_userlist *userlist;
4367     unsigned int i, n;
4368     const int from_opserv = 0; /* for helpserv_notice */
4369     struct helpserv_bot *hs; /* for helpserv_notice */
4370
4371     if ((userlist = dict_find(helpserv_users_byhand_dict, handle->handle, NULL))) {
4372         n=userlist->used;
4373
4374         /* Each time helpserv_del_user is called, that entry is going to be
4375          * taken out of userlist... so this should cope with that */
4376         for (i=0; i < n; i++) {
4377             struct helpserv_user *hs_user=userlist->list[0];
4378             helpserv_del_user(hs_user->hs, hs_user);
4379         }
4380     }
4381
4382     if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, handle->handle, NULL))) {
4383         return;
4384     }
4385
4386     n = hand_reqlist->used;
4387     for (i=0; i < n; i++) {
4388         struct helpserv_request *req=hand_reqlist->list[0];
4389         hs = req->hs;
4390
4391         req->handle = NULL;
4392         req->parent_hand_list = NULL;
4393         helpserv_reqlist_remove(hand_reqlist, req);
4394         if (user && req->helper && (hs->notify >= NOTIFY_HANDLE))
4395             helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_UNREG", req->id, handle->handle, user->nick);
4396
4397         if (!req->user) {
4398             if (!user) {
4399                 /* This is probably an expire. Silently remove everything. */
4400
4401                 char buf[12];
4402                 if (req->helper && (hs->notify >= NOTIFY_DROP))
4403                     helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_UNREGGED", req->id, req->handle->handle);
4404                 sprintf(buf, "%lu", req->id);
4405                 helpserv_log_request(req, "Account unregistered");
4406                 dict_remove(req->hs->requests, buf);
4407             } else if (user->handle_info == handle) {
4408                 req->user = user;
4409                 if (!(req->parent_nick_list = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
4410                     req->parent_nick_list = helpserv_reqlist_alloc();
4411                     dict_insert(helpserv_reqs_bynick_dict, user->nick, req->parent_nick_list);
4412                 }
4413                 helpserv_reqlist_append(req->parent_nick_list, req);
4414
4415                 if (hs->persist_lengths[PERSIST_T_REQUEST] == PERSIST_CLOSE)
4416                     helpserv_msguser(req->user, "HSMSG_REQ_WARN_UNREG", handle->handle, hs->helpchan->name, req->id);
4417             } else {
4418                 if (handle->users) {
4419                     req->user = handle->users;
4420
4421                     if (!(req->parent_nick_list = dict_find(helpserv_reqs_bynick_dict, req->user->nick, NULL))) {
4422                         req->parent_nick_list = helpserv_reqlist_alloc();
4423                         dict_insert(helpserv_reqs_bynick_dict, req->user->nick, req->parent_nick_list);
4424                     }
4425                     helpserv_reqlist_append(req->parent_nick_list, req);
4426
4427                     helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED_UNREG", handle->handle, hs->helpchan->name, req->id);
4428                     if (req->helper && (hs->notify >= NOTIFY_USER))
4429                         helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_MOVE", req->id, handle->handle, req->user->nick);
4430                 } else {
4431                     char buf[12];
4432
4433                     helpserv_notice(user, "HSMSG_REQ_DROPPED_UNREG", handle->handle, hs->helpchan->name, req->id);
4434                     if (req->helper && (hs->notify >= NOTIFY_DROP))
4435                         helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_UNREGGED", req->id, req->handle->handle);
4436                     sprintf(buf, "%lu", req->id);
4437                     helpserv_log_request(req, "Account unregistered");
4438                     dict_remove(req->hs->requests, buf);
4439                 }
4440             }
4441         }
4442     }
4443
4444     dict_remove(helpserv_reqs_byhand_dict, handle->handle);
4445 }
4446
4447 static void handle_nickserv_merge(struct userNode *user, struct handle_info *handle_to, struct handle_info *handle_from) {
4448     struct helpserv_reqlist *reqlist_from, *reqlist_to;
4449     unsigned int i;
4450
4451     reqlist_to = dict_find(helpserv_reqs_byhand_dict, handle_to->handle, NULL);
4452
4453     if ((reqlist_from = dict_find(helpserv_reqs_byhand_dict, handle_from->handle, NULL))) {
4454         for (i=0; i < reqlist_from->used; i++) {
4455             struct helpserv_request *req=reqlist_from->list[i];
4456
4457             if (!reqlist_to) {
4458                 reqlist_to = helpserv_reqlist_alloc();
4459                 dict_insert(helpserv_reqs_byhand_dict, handle_to->handle, reqlist_to);
4460             }
4461             req->parent_hand_list = reqlist_to;
4462             req->handle = handle_to;
4463             helpserv_reqlist_append(reqlist_to, req);
4464         }
4465         dict_remove(helpserv_reqs_byhand_dict, handle_from->handle);
4466     }
4467
4468     if (reqlist_to) {
4469         for (i=0; i < reqlist_to->used; i++) {
4470             struct helpserv_request *req=reqlist_to->list[i];
4471
4472             if (req->helper && (req->hs->notify >= NOTIFY_HANDLE)) {
4473                 helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_MERGE", req->id, handle_to->handle, handle_from->handle, user->nick);
4474             }
4475         }
4476     }
4477 }
4478
4479 static void handle_nickserv_allowauth(struct userNode *user, struct userNode *target, struct handle_info *handle) {
4480     struct helpserv_reqlist *reqlist;
4481     unsigned int i;
4482
4483     if ((reqlist = dict_find(helpserv_reqs_bynick_dict, target->nick, NULL))) {
4484         for (i=0; i < reqlist->used; i++) {
4485             struct helpserv_request *req=reqlist->list[i];
4486
4487             if (req->helper && (req->hs->notify >= NOTIFY_HANDLE)) {
4488                 if (handle) {
4489                     helpserv_notify(req->helper, "HSMSG_NOTIFY_ALLOWAUTH", req->id, target->nick, user->nick, handle->handle);
4490                 } else {
4491                     helpserv_notify(req->helper, "HSMSG_NOTIFY_UNALLOWAUTH", req->id, target->nick, user->nick);
4492                 }
4493             }
4494         }
4495     }
4496 }
4497
4498 static void handle_nickserv_failpw(struct userNode *user, struct handle_info *handle) {
4499     struct helpserv_reqlist *reqlist;
4500     unsigned int i;
4501
4502     if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
4503         for (i=0; i < reqlist->used; i++) {
4504             struct helpserv_request *req=reqlist->list[i];
4505             if (req->helper && (req->hs->notify >= NOTIFY_HANDLE))
4506                 helpserv_notify(req->helper, "HSMSG_NOTIFY_FAILPW", req->id, user->nick, handle->handle);
4507         }
4508     }
4509 }
4510
4511 static unsigned long helpserv_next_stats(time_t after_when) {
4512     struct tm *timeinfo = localtime(&after_when);
4513
4514     /* This works because mktime(3) says it will accept out-of-range values
4515      * and fix them for us. tm_wday and tm_yday are ignored. */
4516     timeinfo->tm_mday++;
4517
4518     /* We want to run stats at midnight (local time). */
4519     timeinfo->tm_sec = timeinfo->tm_min = timeinfo->tm_hour = 0;
4520
4521     return mktime(timeinfo);
4522 }
4523
4524 /* If data != NULL, then don't add to the timeq */
4525 static void helpserv_run_stats(unsigned long when) {
4526     struct helpserv_bot *hs;
4527     struct helpserv_user *hs_user;
4528     time_t feh;
4529     unsigned int day;
4530     int i;
4531     dict_iterator_t it, it2;
4532
4533     last_stats_update = when;
4534     feh = when;
4535     day = localtime(&feh)->tm_wday;
4536     for (it=dict_first(helpserv_bots_dict); it; it=iter_next(it)) {
4537         hs = iter_data(it);
4538
4539         for (it2=dict_first(hs->users); it2; it2=iter_next(it2)) {
4540             hs_user = iter_data(it2);
4541
4542             /* Skip the helper if it's not their week-start day. */
4543             if (hs_user->week_start != day)
4544                 continue;
4545
4546             /* Adjust their credit if they are in-channel at rollover. */
4547             if (hs_user->join_time) {
4548                 hs_user->time_per_week[0] += when - hs_user->join_time;
4549                 hs_user->time_per_week[4] += when - hs_user->join_time;
4550                 hs_user->join_time = when;
4551             }
4552
4553             /* Shift everything */
4554             for (i=3; i > 0; i--) {
4555                 hs_user->time_per_week[i] = hs_user->time_per_week[i-1];
4556                 hs_user->picked_up[i] = hs_user->picked_up[i-1];
4557                 hs_user->closed[i] = hs_user->closed[i-1];
4558                 hs_user->reassigned_from[i] = hs_user->reassigned_from[i-1];
4559                 hs_user->reassigned_to[i] = hs_user->reassigned_to[i-1];
4560             }
4561
4562             /* Reset it for this week */
4563             hs_user->time_per_week[0] = hs_user->picked_up[0] = hs_user->closed[0] = hs_user->reassigned_from[0] = hs_user->reassigned_to[0] = 0;
4564         }
4565     }
4566 }
4567
4568 static void helpserv_timed_run_stats(UNUSED_ARG(void *data)) {
4569     helpserv_run_stats(now);
4570     timeq_add(helpserv_next_stats(now), helpserv_timed_run_stats, data);
4571 }
4572
4573 static void
4574 helpserv_define_option(const char *name, helpserv_option_func_t *func) {
4575     dict_insert(helpserv_option_dict, name, func);
4576 }
4577
4578 static void helpserv_db_cleanup(void) {
4579     shutting_down=1;
4580     unreg_part_func(handle_part);
4581     unreg_del_user_func(handle_quit);
4582     close_helpfile(helpserv_helpfile);
4583     dict_delete(helpserv_func_dict);
4584     dict_delete(helpserv_option_dict);
4585     dict_delete(helpserv_usercmd_dict);
4586     dict_delete(helpserv_bots_dict);
4587     dict_delete(helpserv_bots_bychan_dict);
4588     dict_delete(helpserv_reqs_bynick_dict);
4589     dict_delete(helpserv_reqs_byhand_dict);
4590     dict_delete(helpserv_users_byhand_dict);
4591
4592     if (reqlog_f)
4593         fclose(reqlog_f);
4594 }
4595
4596 int helpserv_init() {
4597     HS_LOG = log_register_type("HelpServ", "file:helpserv.log");
4598     conf_register_reload(helpserv_conf_read);
4599
4600     helpserv_func_dict = dict_new();
4601     dict_set_free_data(helpserv_func_dict, free);
4602     helpserv_define_func("HELP", cmd_help, HlNone, CMD_NOT_OVERRIDE|CMD_IGNORE_EVENT);
4603     helpserv_define_func("LIST", cmd_list, HlTrial, CMD_NEED_BOT|CMD_IGNORE_EVENT);
4604     helpserv_define_func("NEXT", cmd_next, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
4605     helpserv_define_func("PICKUP", cmd_pickup, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
4606     helpserv_define_func("REASSIGN", cmd_reassign, HlManager, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
4607     helpserv_define_func("CLOSE", cmd_close, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
4608     helpserv_define_func("SHOW", cmd_show, HlTrial, CMD_NEED_BOT|CMD_IGNORE_EVENT);
4609     helpserv_define_func("ADDNOTE", cmd_addnote, HlTrial, CMD_NEED_BOT);
4610     helpserv_define_func("ADDOWNER", cmd_addowner, HlOper, CMD_NEED_BOT|CMD_FROM_OPSERV_ONLY);
4611     helpserv_define_func("DELOWNER", cmd_deluser, HlOper, CMD_NEED_BOT|CMD_FROM_OPSERV_ONLY);
4612     helpserv_define_func("ADDTRIAL", cmd_addtrial, HlManager, CMD_NEED_BOT);
4613     helpserv_define_func("ADDHELPER", cmd_addhelper, HlManager, CMD_NEED_BOT);
4614     helpserv_define_func("ADDMANAGER", cmd_addmanager, HlOwner, CMD_NEED_BOT);
4615     helpserv_define_func("GIVEOWNERSHIP", cmd_giveownership, HlOwner, CMD_NEED_BOT);
4616     helpserv_define_func("DELUSER", cmd_deluser, HlManager, CMD_NEED_BOT);
4617     helpserv_define_func("HELPERS", cmd_helpers, HlNone, CMD_NEED_BOT);
4618     helpserv_define_func("WLIST", cmd_wlist, HlNone, CMD_NEED_BOT);
4619     helpserv_define_func("MLIST", cmd_mlist, HlNone, CMD_NEED_BOT);
4620     helpserv_define_func("HLIST", cmd_hlist, HlNone, CMD_NEED_BOT);
4621     helpserv_define_func("TLIST", cmd_tlist, HlNone, CMD_NEED_BOT);
4622     helpserv_define_func("CLVL", cmd_clvl, HlManager, CMD_NEED_BOT);
4623     helpserv_define_func("PAGE", cmd_page, HlTrial, CMD_NEED_BOT);
4624     helpserv_define_func("SET", cmd_set, HlHelper, CMD_NEED_BOT);
4625     helpserv_define_func("STATS", cmd_stats, HlTrial, CMD_NEED_BOT);
4626     helpserv_define_func("STATSREPORT", cmd_statsreport, HlManager, CMD_NEED_BOT);
4627     helpserv_define_func("UNREGISTER", cmd_unregister, HlOwner, CMD_NEED_BOT);
4628     helpserv_define_func("READHELP", cmd_readhelp, HlOper, CMD_FROM_OPSERV_ONLY);
4629     helpserv_define_func("REGISTER", cmd_register, HlOper, CMD_FROM_OPSERV_ONLY);
4630     helpserv_define_func("MOVE", cmd_move, HlOper, CMD_FROM_OPSERV_ONLY|CMD_NEED_BOT);
4631     helpserv_define_func("BOTS", cmd_bots, HlOper, CMD_FROM_OPSERV_ONLY|CMD_IGNORE_EVENT);
4632     helpserv_define_func("EXPIRE", cmd_expire, HlOper, CMD_FROM_OPSERV_ONLY);
4633     helpserv_define_func("WEEKSTART", cmd_weekstart, HlTrial, CMD_NEED_BOT);
4634     helpserv_define_func("MODSTATS", cmd_modstats, HlOwner, CMD_NEED_BOT);
4635
4636     helpserv_option_dict = dict_new();
4637     helpserv_define_option("PAGETARGET", opt_pagetarget_command);
4638     helpserv_define_option("ALERTPAGETARGET", opt_pagetarget_alert);
4639     helpserv_define_option("STATUSPAGETARGET", opt_pagetarget_status);
4640     helpserv_define_option("PAGE", opt_pagetype);
4641     helpserv_define_option("PAGETYPE", opt_pagetype);
4642     helpserv_define_option("ALERTPAGETYPE", opt_alert_page_type);
4643     helpserv_define_option("STATUSPAGETYPE", opt_status_page_type);
4644     helpserv_define_option("GREETING", opt_greeting);
4645     helpserv_define_option("REQOPENED", opt_req_opened);
4646     helpserv_define_option("REQASSIGNED", opt_req_assigned);
4647     helpserv_define_option("REQCLOSED", opt_req_closed);
4648     helpserv_define_option("IDLEDELAY", opt_idle_delay);
4649     helpserv_define_option("WHINEDELAY", opt_whine_delay);
4650     helpserv_define_option("WHINEINTERVAL", opt_whine_interval);
4651     helpserv_define_option("EMPTYINTERVAL", opt_empty_interval);
4652     helpserv_define_option("STALEDELAY", opt_stale_delay);
4653     helpserv_define_option("REQPERSIST", opt_request_persistence);
4654     helpserv_define_option("HELPERPERSIST", opt_helper_persistence);
4655     helpserv_define_option("NOTIFICATION", opt_notification);
4656     helpserv_define_option("REQMAXLEN", opt_req_maxlen);
4657     helpserv_define_option("IDWRAP", opt_id_wrap);
4658     helpserv_define_option("PRIVMSGONLY", opt_privmsg_only);
4659     helpserv_define_option("REQONJOIN", opt_req_on_join);
4660     helpserv_define_option("AUTOVOICE", opt_auto_voice);
4661     helpserv_define_option("AUTODEVOICE", opt_auto_devoice);
4662
4663     helpserv_usercmd_dict = dict_new();
4664     dict_insert(helpserv_usercmd_dict, "WAIT", usercmd_wait);
4665
4666     helpserv_bots_dict = dict_new();
4667     dict_set_free_data(helpserv_bots_dict, helpserv_free_bot);
4668
4669     helpserv_bots_bychan_dict = dict_new();
4670     dict_set_free_data(helpserv_bots_bychan_dict, helpserv_botlist_free);
4671
4672     helpserv_reqs_bynick_dict = dict_new();
4673     dict_set_free_data(helpserv_reqs_bynick_dict, helpserv_reqlist_free);
4674     helpserv_reqs_byhand_dict = dict_new();
4675     dict_set_free_data(helpserv_reqs_byhand_dict, helpserv_reqlist_free);
4676
4677     helpserv_users_byhand_dict = dict_new();
4678     dict_set_free_data(helpserv_users_byhand_dict, helpserv_userlist_free);
4679
4680     saxdb_register("HelpServ", helpserv_saxdb_read, helpserv_saxdb_write);
4681     helpserv_helpfile_read();
4682
4683     /* Make up for downtime... though this will only really affect the
4684      * time_per_week */
4685     if (last_stats_update && (helpserv_next_stats(last_stats_update) < now)) {
4686         unsigned long statsrun = last_stats_update;
4687         while ((statsrun = helpserv_next_stats(statsrun)) < now)
4688             helpserv_run_stats(statsrun);
4689     }
4690     timeq_add(helpserv_next_stats(now), helpserv_timed_run_stats, NULL);
4691
4692     reg_join_func(handle_join);
4693     reg_part_func(handle_part); /* also deals with kick */
4694     reg_nick_change_func(handle_nickchange);
4695     reg_del_user_func(handle_quit);
4696
4697     reg_auth_func(handle_nickserv_auth);
4698     reg_handle_rename_func(handle_nickserv_rename);
4699     reg_unreg_func(handle_nickserv_unreg);
4700     reg_allowauth_func(handle_nickserv_allowauth);
4701     reg_failpw_func(handle_nickserv_failpw);
4702     reg_handle_merge_func(handle_nickserv_merge);
4703
4704     reg_exit_func(helpserv_db_cleanup);
4705
4706     helpserv_module = module_register("helpserv", HS_LOG, HELPSERV_HELPFILE_NAME, helpserv_expand_variable);
4707     modcmd_register(helpserv_module, "helpserv", cmd_helpserv, 1, MODCMD_REQUIRE_AUTHED|MODCMD_NO_LOG|MODCMD_NO_DEFAULT_BIND, "level", "800", NULL);
4708     message_register_table(msgtab);
4709     return 1;
4710 }
4711
4712 int
4713 helpserv_finalize(void) {
4714     return 1;
4715 }