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