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