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