]> git.r.bdr.sh - rbdr/mobius/blame - internal/mobius/transaction_handlers.go
Add downloads as well
[rbdr/mobius] / internal / mobius / transaction_handlers.go
CommitLineData
fd740bc4
JH
1package mobius
2
3import (
4 "bufio"
5 "bytes"
6 "encoding/binary"
7 "fmt"
8 "github.com/jhalter/mobius/hotline"
9 "golang.org/x/text/encoding/charmap"
10 "io"
11 "math/big"
12 "os"
13 "path"
14 "path/filepath"
15 "strings"
16 "time"
17)
18
19// Converts bytes from Mac Roman encoding to UTF-8
20var txtDecoder = charmap.Macintosh.NewDecoder()
21
22// Converts bytes from UTF-8 to Mac Roman encoding
23var txtEncoder = charmap.Macintosh.NewEncoder()
24
25// Assign functions to handle specific Hotline transaction types
26func RegisterHandlers(srv *hotline.Server) {
27 srv.HandleFunc(hotline.TranAgreed, HandleTranAgreed)
28 srv.HandleFunc(hotline.TranChatSend, HandleChatSend)
29 srv.HandleFunc(hotline.TranDelNewsArt, HandleDelNewsArt)
30 srv.HandleFunc(hotline.TranDelNewsItem, HandleDelNewsItem)
8cd28eb8 31 srv.HandleFunc(hotline.TranDeleteFile, HandleDeleteFileWithUserFolders)
fd740bc4
JH
32 srv.HandleFunc(hotline.TranDeleteUser, HandleDeleteUser)
33 srv.HandleFunc(hotline.TranDisconnectUser, HandleDisconnectUser)
ba201a22
RBR
34 srv.HandleFunc(hotline.TranDownloadFile, HandleDownloadFileWithUserFolders)
35 srv.HandleFunc(hotline.TranDownloadFldr, HandleDownloadFolderWithUserFolders)
fd740bc4
JH
36 srv.HandleFunc(hotline.TranGetClientInfoText, HandleGetClientInfoText)
37 srv.HandleFunc(hotline.TranGetFileInfo, HandleGetFileInfo)
7bd6f5ad 38 srv.HandleFunc(hotline.TranGetFileNameList, HandleGetFileNameListWithUserFolders)
fd740bc4
JH
39 srv.HandleFunc(hotline.TranGetMsgs, HandleGetMsgs)
40 srv.HandleFunc(hotline.TranGetNewsArtData, HandleGetNewsArtData)
41 srv.HandleFunc(hotline.TranGetNewsArtNameList, HandleGetNewsArtNameList)
42 srv.HandleFunc(hotline.TranGetNewsCatNameList, HandleGetNewsCatNameList)
43 srv.HandleFunc(hotline.TranGetUser, HandleGetUser)
44 srv.HandleFunc(hotline.TranGetUserNameList, HandleGetUserNameList)
45 srv.HandleFunc(hotline.TranInviteNewChat, HandleInviteNewChat)
46 srv.HandleFunc(hotline.TranInviteToChat, HandleInviteToChat)
47 srv.HandleFunc(hotline.TranJoinChat, HandleJoinChat)
48 srv.HandleFunc(hotline.TranKeepAlive, HandleKeepAlive)
49 srv.HandleFunc(hotline.TranLeaveChat, HandleLeaveChat)
50 srv.HandleFunc(hotline.TranListUsers, HandleListUsers)
51 srv.HandleFunc(hotline.TranMoveFile, HandleMoveFile)
52 srv.HandleFunc(hotline.TranNewFolder, HandleNewFolder)
53 srv.HandleFunc(hotline.TranNewNewsCat, HandleNewNewsCat)
54 srv.HandleFunc(hotline.TranNewNewsFldr, HandleNewNewsFldr)
55 srv.HandleFunc(hotline.TranNewUser, HandleNewUser)
56 srv.HandleFunc(hotline.TranUpdateUser, HandleUpdateUser)
57 srv.HandleFunc(hotline.TranOldPostNews, HandleTranOldPostNews)
58 srv.HandleFunc(hotline.TranPostNewsArt, HandlePostNewsArt)
59 srv.HandleFunc(hotline.TranRejectChatInvite, HandleRejectChatInvite)
60 srv.HandleFunc(hotline.TranSendInstantMsg, HandleSendInstantMsg)
61 srv.HandleFunc(hotline.TranSetChatSubject, HandleSetChatSubject)
62 srv.HandleFunc(hotline.TranMakeFileAlias, HandleMakeAlias)
63 srv.HandleFunc(hotline.TranSetClientUserInfo, HandleSetClientUserInfo)
64 srv.HandleFunc(hotline.TranSetFileInfo, HandleSetFileInfo)
65 srv.HandleFunc(hotline.TranSetUser, HandleSetUser)
7bd6f5ad
RBR
66 srv.HandleFunc(hotline.TranUploadFile, HandleUploadFileWithUserFolders)
67 srv.HandleFunc(hotline.TranUploadFldr, HandleUploadFolderWithUserFolders)
fd740bc4
JH
68 srv.HandleFunc(hotline.TranUserBroadcast, HandleUserBroadcast)
69 srv.HandleFunc(hotline.TranDownloadBanner, HandleDownloadBanner)
70}
71
72func HandleChatSend(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
73 if !cc.Authorize(hotline.AccessSendChat) {
74 return cc.NewErrReply(t, "You are not allowed to participate in chat.")
75 }
76
77 // Truncate long usernames
78 // %13.13s: This means a string that is right-aligned in a field of 13 characters.
79 // If the string is longer than 13 characters, it will be truncated to 13 characters.
80 formattedMsg := fmt.Sprintf("\r%13.13s: %s", cc.UserName, t.GetField(hotline.FieldData).Data)
81
82 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
83 // *** Halcyon does stuff
84 // This is indicated by the presence of the optional field FieldChatOptions set to a value of 1.
85 // Most clients do not send this option for normal chat messages.
86 if t.GetField(hotline.FieldChatOptions).Data != nil && bytes.Equal(t.GetField(hotline.FieldChatOptions).Data, []byte{0, 1}) {
87 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(hotline.FieldData).Data)
88 }
89
90 // Truncate the message to the limit. This does not handle the edge case of a string ending on multibyte character.
91 formattedMsg = formattedMsg[:min(len(formattedMsg), hotline.LimitChatMsg)]
92
93 // The ChatID field is used to identify messages as belonging to a private chat.
94 // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
95 chatID := t.GetField(hotline.FieldChatID).Data
96 if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
97
98 // send the message to all connected clients of the private chat
99 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
100 res = append(res, hotline.NewTransaction(
101 hotline.TranChatMsg,
102 c.ID,
103 hotline.NewField(hotline.FieldChatID, chatID),
104 hotline.NewField(hotline.FieldData, []byte(formattedMsg)),
105 ))
106 }
107 return res
108 }
109
110 //cc.Server.mux.Lock()
111 for _, c := range cc.Server.ClientMgr.List() {
112 if c == nil || cc.Account == nil {
113 continue
114 }
115 // Skip clients that do not have the read chat permission.
116 if c.Authorize(hotline.AccessReadChat) {
117 res = append(res, hotline.NewTransaction(hotline.TranChatMsg, c.ID, hotline.NewField(hotline.FieldData, []byte(formattedMsg))))
118 }
119 }
120 //cc.Server.mux.Unlock()
121
122 return res
123}
124
125// HandleSendInstantMsg sends instant message to the user on the current server.
126// Fields used in the request:
127//
128// 103 User Type
129// 113 Options
130// One of the following values:
131// - User message (myOpt_UserMessage = 1)
132// - Refuse message (myOpt_RefuseMessage = 2)
133// - Refuse chat (myOpt_RefuseChat = 3)
134// - Automatic response (myOpt_AutomaticResponse = 4)"
135// 101 Data Optional
136// 214 Quoting message Optional
137//
138// Fields used in the reply:
139// None
140func HandleSendInstantMsg(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
141 if !cc.Authorize(hotline.AccessSendPrivMsg) {
142 return cc.NewErrReply(t, "You are not allowed to send private messages.")
143 }
144
145 msg := t.GetField(hotline.FieldData)
146 userID := t.GetField(hotline.FieldUserID)
147
148 reply := hotline.NewTransaction(
149 hotline.TranServerMsg,
150 [2]byte(userID.Data),
151 hotline.NewField(hotline.FieldData, msg.Data),
152 hotline.NewField(hotline.FieldUserName, cc.UserName),
153 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
154 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
155 )
156
157 // Later versions of Hotline include the original message in the FieldQuotingMsg field so
158 // the receiving client can display both the received message and what it is in reply to
159 if t.GetField(hotline.FieldQuotingMsg).Data != nil {
160 reply.Fields = append(reply.Fields, hotline.NewField(hotline.FieldQuotingMsg, t.GetField(hotline.FieldQuotingMsg).Data))
161 }
162
163 otherClient := cc.Server.ClientMgr.Get([2]byte(userID.Data))
164 if otherClient == nil {
165 return res
166 }
167
168 // Check if target user has "Refuse private messages" flag
169 if otherClient.Flags.IsSet(hotline.UserFlagRefusePM) {
170 res = append(res,
171 hotline.NewTransaction(
172 hotline.TranServerMsg,
173 cc.ID,
174 hotline.NewField(hotline.FieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
175 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
176 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
177 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
178 ),
179 )
180 } else {
181 res = append(res, reply)
182 }
183
184 // Respond with auto reply if other client has it enabled
185 if len(otherClient.AutoReply) > 0 {
186 res = append(res,
187 hotline.NewTransaction(
188 hotline.TranServerMsg,
189 cc.ID,
190 hotline.NewField(hotline.FieldData, otherClient.AutoReply),
191 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
192 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
193 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
194 ),
195 )
196 }
197
198 return append(res, cc.NewReply(t))
199}
200
201var fileTypeFLDR = [4]byte{0x66, 0x6c, 0x64, 0x72}
202
203func HandleGetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
204 fileName := t.GetField(hotline.FieldFileName).Data
205 filePath := t.GetField(hotline.FieldFilePath).Data
206
dcd23d53 207 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
208 if err != nil {
209 return res
210 }
211
212 fw, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
213 if err != nil {
214 return res
215 }
216
217 encodedName, err := txtEncoder.String(fw.Name)
218 if err != nil {
219 return res
220 }
221
222 fields := []hotline.Field{
223 hotline.NewField(hotline.FieldFileName, []byte(encodedName)),
224 hotline.NewField(hotline.FieldFileTypeString, fw.Ffo.FlatFileInformationFork.FriendlyType()),
225 hotline.NewField(hotline.FieldFileCreatorString, fw.Ffo.FlatFileInformationFork.FriendlyCreator()),
226 hotline.NewField(hotline.FieldFileType, fw.Ffo.FlatFileInformationFork.TypeSignature[:]),
227 hotline.NewField(hotline.FieldFileCreateDate, fw.Ffo.FlatFileInformationFork.CreateDate[:]),
228 hotline.NewField(hotline.FieldFileModifyDate, fw.Ffo.FlatFileInformationFork.ModifyDate[:]),
229 }
230
231 // Include the optional FileComment field if there is a comment.
232 if len(fw.Ffo.FlatFileInformationFork.Comment) != 0 {
233 fields = append(fields, hotline.NewField(hotline.FieldFileComment, fw.Ffo.FlatFileInformationFork.Comment))
234 }
235
236 // Include the FileSize field for files.
237 if fw.Ffo.FlatFileInformationFork.TypeSignature != fileTypeFLDR {
238 fields = append(fields, hotline.NewField(hotline.FieldFileSize, fw.TotalSize()))
239 }
240
241 res = append(res, cc.NewReply(t, fields...))
242 return res
243}
244
245// HandleSetFileInfo updates a file or folder Name and/or comment from the Get Info window
246// Fields used in the request:
247// * 201 File Name
248// * 202 File path Optional
249// * 211 File new Name Optional
250// * 210 File comment Optional
251// Fields used in the reply: None
252func HandleSetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
253 fileName := t.GetField(hotline.FieldFileName).Data
254 filePath := t.GetField(hotline.FieldFilePath).Data
255
dcd23d53 256 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
257 if err != nil {
258 return res
259 }
260
261 fi, err := cc.Server.FS.Stat(fullFilePath)
262 if err != nil {
263 return res
264 }
265
266 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
267 if err != nil {
268 return res
269 }
270 if t.GetField(hotline.FieldFileComment).Data != nil {
271 switch mode := fi.Mode(); {
272 case mode.IsDir():
273 if !cc.Authorize(hotline.AccessSetFolderComment) {
274 return cc.NewErrReply(t, "You are not allowed to set comments for folders.")
275 }
276 case mode.IsRegular():
277 if !cc.Authorize(hotline.AccessSetFileComment) {
278 return cc.NewErrReply(t, "You are not allowed to set comments for files.")
279 }
280 }
281
282 if err := hlFile.Ffo.FlatFileInformationFork.SetComment(t.GetField(hotline.FieldFileComment).Data); err != nil {
283 return res
284 }
285 w, err := hlFile.InfoForkWriter()
286 if err != nil {
287 return res
288 }
289 _, err = io.Copy(w, &hlFile.Ffo.FlatFileInformationFork)
290 if err != nil {
291 return res
292 }
293 }
294
dcd23d53 295 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, t.GetField(hotline.FieldFileNewName).Data)
fd740bc4
JH
296 if err != nil {
297 return nil
298 }
299
300 fileNewName := t.GetField(hotline.FieldFileNewName).Data
301
302 if fileNewName != nil {
303 switch mode := fi.Mode(); {
304 case mode.IsDir():
305 if !cc.Authorize(hotline.AccessRenameFolder) {
306 return cc.NewErrReply(t, "You are not allowed to rename folders.")
307 }
308 err = os.Rename(fullFilePath, fullNewFilePath)
309 if os.IsNotExist(err) {
310 return cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found.")
311
312 }
313 case mode.IsRegular():
314 if !cc.Authorize(hotline.AccessRenameFile) {
315 return cc.NewErrReply(t, "You are not allowed to rename files.")
316 }
dcd23d53 317 fileDir, err := hotline.ReadPath(cc.FileRoot(), filePath, []byte{})
fd740bc4
JH
318 if err != nil {
319 return nil
320 }
321 hlFile.Name, err = txtDecoder.String(string(fileNewName))
322 if err != nil {
323 return res
324 }
325
326 err = hlFile.Move(fileDir)
327 if os.IsNotExist(err) {
328 return cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found.")
329 }
330 if err != nil {
331 return res
332 }
333 }
334 }
335
336 res = append(res, cc.NewReply(t))
337 return res
338}
339
340// HandleDeleteFile deletes a file or folder
341// Fields used in the request:
342// * 201 File Name
343// * 202 File path
344// Fields used in the reply: none
345func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
346 fileName := t.GetField(hotline.FieldFileName).Data
347 filePath := t.GetField(hotline.FieldFilePath).Data
348
8cd28eb8
RBR
349 var fp hotline.FilePath
350 if filePath != nil {
351 if _, err := fp.Write(filePath); err != nil {
352 return res
353 }
354 }
355
dcd23d53 356 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
357 if err != nil {
358 return res
359 }
360
361 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
362 if err != nil {
363 return res
364 }
365
366 fi, err := hlFile.DataFile()
367 if err != nil {
368 return cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found.")
369 }
370
371 switch mode := fi.Mode(); {
372 case mode.IsDir():
8cd28eb8 373 if !cc.Authorize(hotline.AccessDeleteFolder) && !fp.IsUserDir() {
fd740bc4
JH
374 return cc.NewErrReply(t, "You are not allowed to delete folders.")
375 }
376 case mode.IsRegular():
8cd28eb8 377 if !cc.Authorize(hotline.AccessDeleteFile) && !fp.IsUserDir() {
fd740bc4
JH
378 return cc.NewErrReply(t, "You are not allowed to delete files.")
379 }
380 }
381
382 if err := hlFile.Delete(); err != nil {
383 return res
384 }
385
386 res = append(res, cc.NewReply(t))
387 return res
388}
389
390// HandleMoveFile moves files or folders. Note: seemingly not documented
391func HandleMoveFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
392 fileName := string(t.GetField(hotline.FieldFileName).Data)
393
dcd23d53 394 filePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
fd740bc4
JH
395 if err != nil {
396 return res
397 }
398
dcd23d53 399 fileNewPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFileNewPath).Data, nil)
fd740bc4
JH
400 if err != nil {
401 return res
402 }
403
404 cc.Logger.Info("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
405
406 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, filePath, 0)
407 if err != nil {
408 return res
409 }
410
411 fi, err := hlFile.DataFile()
412 if err != nil {
413 return cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found.")
414 }
415 switch mode := fi.Mode(); {
416 case mode.IsDir():
417 if !cc.Authorize(hotline.AccessMoveFolder) {
418 return cc.NewErrReply(t, "You are not allowed to move folders.")
419 }
420 case mode.IsRegular():
421 if !cc.Authorize(hotline.AccessMoveFile) {
422 return cc.NewErrReply(t, "You are not allowed to move files.")
423 }
424 }
425 if err := hlFile.Move(fileNewPath); err != nil {
426 return res
427 }
0ee4d86e 428 // TODO: handle other possible errors; e.g. file delete fails due to permission issue
fd740bc4
JH
429
430 res = append(res, cc.NewReply(t))
431 return res
432}
433
434func HandleNewFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
435 if !cc.Authorize(hotline.AccessCreateFolder) {
436 return cc.NewErrReply(t, "You are not allowed to create folders.")
437 }
438 folderName := string(t.GetField(hotline.FieldFileName).Data)
439
440 folderName = path.Join("/", folderName)
441
442 var subPath string
443
444 // FieldFilePath is only present for nested paths
445 if t.GetField(hotline.FieldFilePath).Data != nil {
446 var newFp hotline.FilePath
447 _, err := newFp.Write(t.GetField(hotline.FieldFilePath).Data)
448 if err != nil {
449 return res
450 }
451
452 for _, pathItem := range newFp.Items {
453 subPath = filepath.Join("/", subPath, string(pathItem.Name))
454 }
455 }
dcd23d53 456 newFolderPath := path.Join(cc.FileRoot(), subPath, folderName)
fd740bc4
JH
457 newFolderPath, err := txtDecoder.String(newFolderPath)
458 if err != nil {
459 return res
460 }
461
462 // TODO: check path and folder Name lengths
463
464 if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
465 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that Name.", folderName)
466 return cc.NewErrReply(t, msg)
467 }
468
469 if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
470 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
471 return cc.NewErrReply(t, msg)
472 }
473
474 return append(res, cc.NewReply(t))
475}
476
477func HandleSetUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
478 if !cc.Authorize(hotline.AccessModifyUser) {
479 return cc.NewErrReply(t, "You are not allowed to modify accounts.")
480 }
481
482 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
483 userName := string(t.GetField(hotline.FieldUserName).Data)
484
485 newAccessLvl := t.GetField(hotline.FieldUserAccess).Data
486
487 account := cc.Server.AccountManager.Get(login)
488 if account == nil {
489 return cc.NewErrReply(t, "Account not found.")
490 }
491 account.Name = userName
492 copy(account.Access[:], newAccessLvl)
493
494 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
495 // not include FieldUserPassword
496 if t.GetField(hotline.FieldUserPassword).Data == nil {
497 account.Password = hotline.HashAndSalt([]byte(""))
498 }
499
500 if !bytes.Equal([]byte{0}, t.GetField(hotline.FieldUserPassword).Data) {
501 account.Password = hotline.HashAndSalt(t.GetField(hotline.FieldUserPassword).Data)
502 }
503
504 err := cc.Server.AccountManager.Update(*account, account.Login)
505 if err != nil {
506 cc.Logger.Error("Error updating account", "Err", err)
507 }
508
509 // Notify connected clients logged in as the user of the new access level
510 for _, c := range cc.Server.ClientMgr.List() {
511 if c.Account.Login == login {
512 newT := hotline.NewTransaction(hotline.TranUserAccess, c.ID, hotline.NewField(hotline.FieldUserAccess, newAccessLvl))
513 res = append(res, newT)
514
515 if c.Authorize(hotline.AccessDisconUser) {
516 c.Flags.Set(hotline.UserFlagAdmin, 1)
517 } else {
518 c.Flags.Set(hotline.UserFlagAdmin, 0)
519 }
520
521 c.Account.Access = account.Access
522
523 cc.SendAll(
524 hotline.TranNotifyChangeUser,
525 hotline.NewField(hotline.FieldUserID, c.ID[:]),
526 hotline.NewField(hotline.FieldUserFlags, c.Flags[:]),
527 hotline.NewField(hotline.FieldUserName, c.UserName),
528 hotline.NewField(hotline.FieldUserIconID, c.Icon),
529 )
530 }
531 }
532
533 return append(res, cc.NewReply(t))
534}
535
536func HandleGetUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
537 if !cc.Authorize(hotline.AccessOpenUser) {
538 return cc.NewErrReply(t, "You are not allowed to view accounts.")
539 }
540
541 account := cc.Server.AccountManager.Get(string(t.GetField(hotline.FieldUserLogin).Data))
542 if account == nil {
543 return cc.NewErrReply(t, "Account does not exist.")
544 }
545
546 return append(res, cc.NewReply(t,
547 hotline.NewField(hotline.FieldUserName, []byte(account.Name)),
548 hotline.NewField(hotline.FieldUserLogin, hotline.EncodeString(t.GetField(hotline.FieldUserLogin).Data)),
549 hotline.NewField(hotline.FieldUserPassword, []byte(account.Password)),
550 hotline.NewField(hotline.FieldUserAccess, account.Access[:]),
551 ))
552}
553
554func HandleListUsers(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
555 if !cc.Authorize(hotline.AccessOpenUser) {
556 return cc.NewErrReply(t, "You are not allowed to view accounts.")
557 }
558
559 var userFields []hotline.Field
560 for _, acc := range cc.Server.AccountManager.List() {
561 b, err := io.ReadAll(&acc)
562 if err != nil {
563 cc.Logger.Error("Error reading account", "Account", acc.Login, "Err", err)
564 continue
565 }
566
567 userFields = append(userFields, hotline.NewField(hotline.FieldData, b))
568 }
569
570 return append(res, cc.NewReply(t, userFields...))
571}
572
573// HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
574// An update can be a mix of these actions:
575// * Create user
576// * Delete user
577// * Modify user (including renaming the account login)
578//
579// The Transaction sent by the client includes one data field per user that was modified. This data field in turn
580// contains another data field encoded in its payload with a varying number of sub fields depending on which action is
581// performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
582func HandleUpdateUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
583 for _, field := range t.Fields {
584 var subFields []hotline.Field
585
586 // Create a new scanner for parsing incoming bytes into transaction tokens
587 scanner := bufio.NewScanner(bytes.NewReader(field.Data[2:]))
588 scanner.Split(hotline.FieldScanner)
589
590 for i := 0; i < int(binary.BigEndian.Uint16(field.Data[0:2])); i++ {
591 scanner.Scan()
592
593 var field hotline.Field
594 if _, err := field.Write(scanner.Bytes()); err != nil {
595 return res
596 }
597 subFields = append(subFields, field)
598 }
599
600 // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
601 if len(subFields) == 1 {
602 if !cc.Authorize(hotline.AccessDeleteUser) {
603 return cc.NewErrReply(t, "You are not allowed to delete accounts.")
604 }
605
606 login := string(hotline.EncodeString(hotline.GetField(hotline.FieldData, &subFields).Data))
607
608 cc.Logger.Info("DeleteUser", "login", login)
609
610 if err := cc.Server.AccountManager.Delete(login); err != nil {
611 cc.Logger.Error("Error deleting account", "Err", err)
612 return res
613 }
614
615 for _, client := range cc.Server.ClientMgr.List() {
616 if client.Account.Login == login {
617 // "You are logged in with an account which was deleted."
618
619 res = append(res,
620 hotline.NewTransaction(hotline.TranServerMsg, [2]byte{},
621 hotline.NewField(hotline.FieldData, []byte("You are logged in with an account which was deleted.")),
622 hotline.NewField(hotline.FieldChatOptions, []byte{0}),
623 ),
624 )
625
626 go func(c *hotline.ClientConn) {
627 time.Sleep(3 * time.Second)
628 c.Disconnect()
629 }(client)
630 }
631 }
632
633 continue
634 }
635
636 // login of the account to update
637 var accountToUpdate, loginToRename string
638
639 // If FieldData is included, this is a rename operation where FieldData contains the login of the existing
640 // account and FieldUserLogin contains the new login.
641 if hotline.GetField(hotline.FieldData, &subFields) != nil {
642 loginToRename = string(hotline.EncodeString(hotline.GetField(hotline.FieldData, &subFields).Data))
643 }
644 userLogin := string(hotline.EncodeString(hotline.GetField(hotline.FieldUserLogin, &subFields).Data))
645 if loginToRename != "" {
646 accountToUpdate = loginToRename
647 } else {
648 accountToUpdate = userLogin
649 }
650
651 // Check if accountToUpdate has an existing account. If so, we know we are updating an existing user.
652 if acc := cc.Server.AccountManager.Get(accountToUpdate); acc != nil {
653 if loginToRename != "" {
654 cc.Logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
655 } else {
656 cc.Logger.Info("UpdateUser", "login", accountToUpdate)
657 }
658
659 // Account exists, so this is an update action.
660 if !cc.Authorize(hotline.AccessModifyUser) {
661 return cc.NewErrReply(t, "You are not allowed to modify accounts.")
662 }
663
664 // This part is a bit tricky. There are three possibilities:
665 // 1) The transaction is intended to update the password.
666 // In this case, FieldUserPassword is sent with the new password.
667 // 2) The transaction is intended to remove the password.
668 // In this case, FieldUserPassword is not sent.
669 // 3) The transaction updates the users access bits, but not the password.
670 // In this case, FieldUserPassword is sent with zero as the only byte.
671 if hotline.GetField(hotline.FieldUserPassword, &subFields) != nil {
672 newPass := hotline.GetField(hotline.FieldUserPassword, &subFields).Data
673 if !bytes.Equal([]byte{0}, newPass) {
674 acc.Password = hotline.HashAndSalt(newPass)
675 }
676 } else {
677 acc.Password = hotline.HashAndSalt([]byte(""))
678 }
679
680 if hotline.GetField(hotline.FieldUserAccess, &subFields) != nil {
681 copy(acc.Access[:], hotline.GetField(hotline.FieldUserAccess, &subFields).Data)
682 }
683
684 acc.Name = string(hotline.GetField(hotline.FieldUserName, &subFields).Data)
685
686 err := cc.Server.AccountManager.Update(*acc, string(hotline.EncodeString(hotline.GetField(hotline.FieldUserLogin, &subFields).Data)))
687
688 if err != nil {
689 return res
690 }
691 } else {
692 if !cc.Authorize(hotline.AccessCreateUser) {
693 return cc.NewErrReply(t, "You are not allowed to create new accounts.")
694 }
695
696 cc.Logger.Info("CreateUser", "login", userLogin)
697
698 var newAccess hotline.AccessBitmap
699 copy(newAccess[:], hotline.GetField(hotline.FieldUserAccess, &subFields).Data)
700
701 // Prevent account from creating new account with greater permission
702 for i := 0; i < 64; i++ {
703 if newAccess.IsSet(i) {
704 if !cc.Authorize(i) {
705 return cc.NewErrReply(t, "Cannot create account with more access than yourself.")
706 }
707 }
708 }
709
710 account := hotline.NewAccount(
711 userLogin,
712 string(hotline.GetField(hotline.FieldUserName, &subFields).Data),
713 string(hotline.GetField(hotline.FieldUserPassword, &subFields).Data),
714 newAccess,
715 )
716
717 err := cc.Server.AccountManager.Create(*account)
718 if err != nil {
719 return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
720 }
721 }
722 }
723
724 return append(res, cc.NewReply(t))
725}
726
727// HandleNewUser creates a new user account
728func HandleNewUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
729 if !cc.Authorize(hotline.AccessCreateUser) {
730 return cc.NewErrReply(t, "You are not allowed to create new accounts.")
731 }
732
733 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
734
735 // If the account already exists, reply with an error.
736 if account := cc.Server.AccountManager.Get(login); account != nil {
737 return cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login.")
738 }
739
740 var newAccess hotline.AccessBitmap
741 copy(newAccess[:], t.GetField(hotline.FieldUserAccess).Data)
742
743 // Prevent account from creating new account with greater permission
744 for i := 0; i < 64; i++ {
745 if newAccess.IsSet(i) {
746 if !cc.Authorize(i) {
747 return cc.NewErrReply(t, "Cannot create account with more access than yourself.")
748 }
749 }
750 }
751
752 account := hotline.NewAccount(login, string(t.GetField(hotline.FieldUserName).Data), string(t.GetField(hotline.FieldUserPassword).Data), newAccess)
753
754 err := cc.Server.AccountManager.Create(*account)
755 if err != nil {
756 return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
757 }
758
759 return append(res, cc.NewReply(t))
760}
761
762func HandleDeleteUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
763 if !cc.Authorize(hotline.AccessDeleteUser) {
764 return cc.NewErrReply(t, "You are not allowed to delete accounts.")
765 }
766
767 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
768
769 if err := cc.Server.AccountManager.Delete(login); err != nil {
770 cc.Logger.Error("Error deleting account", "Err", err)
771 return res
772 }
773
774 for _, client := range cc.Server.ClientMgr.List() {
775 if client.Account.Login == login {
776 res = append(res,
777 hotline.NewTransaction(hotline.TranServerMsg, client.ID,
778 hotline.NewField(hotline.FieldData, []byte("You are logged in with an account which was deleted.")),
779 hotline.NewField(hotline.FieldChatOptions, []byte{2}),
780 ),
781 )
782
783 go func(c *hotline.ClientConn) {
784 time.Sleep(2 * time.Second)
785 c.Disconnect()
786 }(client)
787 }
788 }
789
790 return append(res, cc.NewReply(t))
791}
792
793// HandleUserBroadcast sends an Administrator Message to all connected clients of the server
794func HandleUserBroadcast(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
795 if !cc.Authorize(hotline.AccessBroadcast) {
796 return cc.NewErrReply(t, "You are not allowed to send broadcast messages.")
797 }
798
799 cc.SendAll(
800 hotline.TranServerMsg,
801 hotline.NewField(hotline.FieldData, t.GetField(hotline.FieldData).Data),
802 hotline.NewField(hotline.FieldChatOptions, []byte{0}),
803 )
804
805 return append(res, cc.NewReply(t))
806}
807
808// HandleGetClientInfoText returns user information for the specific user.
809//
810// Fields used in the request:
811// 103 User Type
812//
813// Fields used in the reply:
814// 102 User Name
815// 101 Data User info text string
816func HandleGetClientInfoText(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
817 if !cc.Authorize(hotline.AccessGetClientInfo) {
818 return cc.NewErrReply(t, "You are not allowed to get client info.")
819 }
820
821 clientID := t.GetField(hotline.FieldUserID).Data
822
823 clientConn := cc.Server.ClientMgr.Get(hotline.ClientID(clientID))
824 if clientConn == nil {
825 return cc.NewErrReply(t, "User not found.")
826 }
827
828 return append(res, cc.NewReply(t,
829 hotline.NewField(hotline.FieldData, []byte(clientConn.String())),
830 hotline.NewField(hotline.FieldUserName, clientConn.UserName),
831 ))
832}
833
834func HandleGetUserNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
835 var fields []hotline.Field
836 for _, c := range cc.Server.ClientMgr.List() {
837 b, err := io.ReadAll(&hotline.User{
838 ID: c.ID,
839 Icon: c.Icon,
840 Flags: c.Flags[:],
841 Name: string(c.UserName),
842 })
843 if err != nil {
844 return nil
845 }
846
847 fields = append(fields, hotline.NewField(hotline.FieldUsernameWithInfo, b))
848 }
849
850 return []hotline.Transaction{cc.NewReply(t, fields...)}
851}
852
853func HandleTranAgreed(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
854 if t.GetField(hotline.FieldUserName).Data != nil {
855 if cc.Authorize(hotline.AccessAnyName) {
856 cc.UserName = t.GetField(hotline.FieldUserName).Data
857 } else {
858 cc.UserName = []byte(cc.Account.Name)
859 }
860 }
861
862 cc.Icon = t.GetField(hotline.FieldUserIconID).Data
863
864 cc.Logger = cc.Logger.With("Name", string(cc.UserName))
865 cc.Logger.Info("Login successful")
866
867 options := t.GetField(hotline.FieldOptions).Data
868 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
869
870 // Check refuse private PM option
871
872 cc.FlagsMU.Lock()
873 defer cc.FlagsMU.Unlock()
874 cc.Flags.Set(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM))
875
876 // Check refuse private chat option
877 cc.Flags.Set(hotline.UserFlagRefusePChat, optBitmap.Bit(hotline.UserOptRefuseChat))
878
879 // Check auto response
880 if optBitmap.Bit(hotline.UserOptAutoResponse) == 1 {
881 cc.AutoReply = t.GetField(hotline.FieldAutomaticResponse).Data
882 }
883
884 trans := cc.NotifyOthers(
885 hotline.NewTransaction(
886 hotline.TranNotifyChangeUser, [2]byte{0, 0},
887 hotline.NewField(hotline.FieldUserName, cc.UserName),
888 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
889 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
890 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
891 ),
892 )
893 res = append(res, trans...)
894
895 if cc.Server.Config.BannerFile != "" {
896 res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte("JPEG"))))
897 }
898
899 res = append(res, cc.NewReply(t))
900
901 return res
902}
903
904// HandleTranOldPostNews updates the flat news
905// Fields used in this request:
906// 101 Data
907func HandleTranOldPostNews(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
908 if !cc.Authorize(hotline.AccessNewsPostArt) {
909 return cc.NewErrReply(t, "You are not allowed to post news.")
910 }
911
912 newsDateTemplate := hotline.NewsDateFormat
913 if cc.Server.Config.NewsDateFormat != "" {
914 newsDateTemplate = cc.Server.Config.NewsDateFormat
915 }
916
917 newsTemplate := hotline.NewsTemplate
918 if cc.Server.Config.NewsDelimiter != "" {
919 newsTemplate = cc.Server.Config.NewsDelimiter
920 }
921
922 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(hotline.FieldData).Data)
923 newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
924
925 _, err := cc.Server.MessageBoard.Write([]byte(newsPost))
926 if err != nil {
927 cc.Logger.Error("error writing news post", "err", err)
928 return nil
929 }
930
931 // Notify all clients of updated news
932 cc.SendAll(
933 hotline.TranNewMsg,
934 hotline.NewField(hotline.FieldData, []byte(newsPost)),
935 )
936
937 return append(res, cc.NewReply(t))
938}
939
940func HandleDisconnectUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
941 if !cc.Authorize(hotline.AccessDisconUser) {
942 return cc.NewErrReply(t, "You are not allowed to disconnect users.")
943 }
944
945 clientID := [2]byte(t.GetField(hotline.FieldUserID).Data)
946 clientConn := cc.Server.ClientMgr.Get(clientID)
947
948 if clientConn.Authorize(hotline.AccessCannotBeDiscon) {
949 return cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected.")
950 }
951
952 // If FieldOptions is set, then the client IP is banned in addition to disconnected.
953 // 00 01 = temporary ban
954 // 00 02 = permanent ban
955 if t.GetField(hotline.FieldOptions).Data != nil {
956 switch t.GetField(hotline.FieldOptions).Data[1] {
957 case 1:
958 // send message: "You are temporarily banned on this server"
959 cc.Logger.Info("Disconnect & temporarily ban " + string(clientConn.UserName))
960
961 res = append(res, hotline.NewTransaction(
962 hotline.TranServerMsg,
963 clientConn.ID,
964 hotline.NewField(hotline.FieldData, []byte("You are temporarily banned on this server")),
965 hotline.NewField(hotline.FieldChatOptions, []byte{0, 0}),
966 ))
967
968 banUntil := time.Now().Add(hotline.BanDuration)
969 ip := strings.Split(clientConn.RemoteAddr, ":")[0]
970
971 err := cc.Server.BanList.Add(ip, &banUntil)
972 if err != nil {
973 cc.Logger.Error("Error saving ban", "err", err)
974 // TODO
975 }
976 case 2:
977 // send message: "You are permanently banned on this server"
978 cc.Logger.Info("Disconnect & ban " + string(clientConn.UserName))
979
980 res = append(res, hotline.NewTransaction(
981 hotline.TranServerMsg,
982 clientConn.ID,
983 hotline.NewField(hotline.FieldData, []byte("You are permanently banned on this server")),
984 hotline.NewField(hotline.FieldChatOptions, []byte{0, 0}),
985 ))
986
987 ip := strings.Split(clientConn.RemoteAddr, ":")[0]
988
989 err := cc.Server.BanList.Add(ip, nil)
990 if err != nil {
9f89cd9f 991 cc.Logger.Error("Error saving ban", "err", err)
fd740bc4
JH
992 }
993 }
994 }
995
fd740bc4
JH
996 go func() {
997 time.Sleep(1 * time.Second)
998 clientConn.Disconnect()
999 }()
1000
1001 return append(res, cc.NewReply(t))
1002}
1003
1004// HandleGetNewsCatNameList returns a list of news categories for a path
1005// Fields used in the request:
1006// 325 News path (Optional)
1007func HandleGetNewsCatNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1008 if !cc.Authorize(hotline.AccessNewsReadArt) {
1009 return cc.NewErrReply(t, "You are not allowed to read news.")
1010 }
1011
1012 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1013 if err != nil {
9f89cd9f
JH
1014 cc.Logger.Error("get news path", "err", err)
1015 return nil
fd740bc4
JH
1016 }
1017
1018 var fields []hotline.Field
1019 for _, cat := range cc.Server.ThreadedNewsMgr.GetCategories(pathStrs) {
1020 b, err := io.ReadAll(&cat)
1021 if err != nil {
9f89cd9f 1022 cc.Logger.Error("get news categories", "err", err)
fd740bc4
JH
1023 }
1024
1025 fields = append(fields, hotline.NewField(hotline.FieldNewsCatListData15, b))
1026 }
1027
1028 return append(res, cc.NewReply(t, fields...))
1029}
1030
1031func HandleNewNewsCat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1032 if !cc.Authorize(hotline.AccessNewsCreateCat) {
1033 return cc.NewErrReply(t, "You are not allowed to create news categories.")
1034 }
1035
1036 name := string(t.GetField(hotline.FieldNewsCatName).Data)
1037 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1038 if err != nil {
1039 return res
1040 }
1041
1042 err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, hotline.NewsCategory)
1043 if err != nil {
1044 cc.Logger.Error("error creating news category", "err", err)
1045 }
1046
1047 return []hotline.Transaction{cc.NewReply(t)}
1048}
1049
1050// Fields used in the request:
1051// 322 News category Name
1052// 325 News path
1053func HandleNewNewsFldr(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1054 if !cc.Authorize(hotline.AccessNewsCreateFldr) {
1055 return cc.NewErrReply(t, "You are not allowed to create news folders.")
1056 }
1057
1058 name := string(t.GetField(hotline.FieldFileName).Data)
1059 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1060 if err != nil {
1061 return res
1062 }
1063
1064 err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, hotline.NewsBundle)
1065 if err != nil {
1066 cc.Logger.Error("error creating news bundle", "err", err)
1067 }
1068
1069 return append(res, cc.NewReply(t))
1070}
1071
1072// HandleGetNewsArtData gets the list of article names at the specified news path.
1073
1074// Fields used in the request:
1075// 325 News path Optional
1076
1077// Fields used in the reply:
1078// 321 News article list data Optional
1079func HandleGetNewsArtNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1080 if !cc.Authorize(hotline.AccessNewsReadArt) {
1081 return cc.NewErrReply(t, "You are not allowed to read news.")
1082 }
1083
1084 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1085 if err != nil {
1086 return res
1087 }
1088
1089 nald := cc.Server.ThreadedNewsMgr.ListArticles(pathStrs)
1090
1091 b, err := io.ReadAll(&nald)
1092 if err != nil {
1093 return res
1094 }
1095
1096 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldNewsArtListData, b)))
1097}
1098
1099// HandleGetNewsArtData requests information about the specific news article.
1100// Fields used in the request:
1101//
1102// Request fields
1103// 325 News path
1104// 326 News article Type
1105// 327 News article data flavor
1106//
1107// Fields used in the reply:
1108// 328 News article title
1109// 329 News article poster
1110// 330 News article date
1111// 331 Previous article Type
1112// 332 Next article Type
1113// 335 Parent article Type
1114// 336 First child article Type
1115// 327 News article data flavor "Should be “text/plain”
1116// 333 News article data Optional (if data flavor is “text/plain”)
1117func HandleGetNewsArtData(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1118 if !cc.Authorize(hotline.AccessNewsReadArt) {
1119 return cc.NewErrReply(t, "You are not allowed to read news.")
1120 }
1121
1122 newsPath, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1123 if err != nil {
1124 return res
1125 }
1126
1127 convertedID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1128 if err != nil {
1129 return res
1130 }
1131
1132 art := cc.Server.ThreadedNewsMgr.GetArticle(newsPath, uint32(convertedID))
1133 if art == nil {
1134 return append(res, cc.NewReply(t))
1135 }
1136
1137 res = append(res, cc.NewReply(t,
1138 hotline.NewField(hotline.FieldNewsArtTitle, []byte(art.Title)),
1139 hotline.NewField(hotline.FieldNewsArtPoster, []byte(art.Poster)),
1140 hotline.NewField(hotline.FieldNewsArtDate, art.Date[:]),
1141 hotline.NewField(hotline.FieldNewsArtPrevArt, art.PrevArt[:]),
1142 hotline.NewField(hotline.FieldNewsArtNextArt, art.NextArt[:]),
1143 hotline.NewField(hotline.FieldNewsArtParentArt, art.ParentArt[:]),
1144 hotline.NewField(hotline.FieldNewsArt1stChildArt, art.FirstChildArt[:]),
1145 hotline.NewField(hotline.FieldNewsArtDataFlav, []byte("text/plain")),
1146 hotline.NewField(hotline.FieldNewsArtData, []byte(art.Data)),
1147 ))
1148 return res
1149}
1150
1151// HandleDelNewsItem deletes a threaded news folder or category.
1152// Fields used in the request:
1153// 325 News path
1154// Fields used in the reply:
1155// None
1156func HandleDelNewsItem(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1157 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
318eb6bc
JH
1158 if err != nil || len(pathStrs) == 0 {
1159 cc.Logger.Error("invalid news path")
1160 return nil
fd740bc4
JH
1161 }
1162
1163 item := cc.Server.ThreadedNewsMgr.NewsItem(pathStrs)
1164
1165 if item.Type == [2]byte{0, 3} {
1166 if !cc.Authorize(hotline.AccessNewsDeleteCat) {
1167 return cc.NewErrReply(t, "You are not allowed to delete news categories.")
1168 }
1169 } else {
1170 if !cc.Authorize(hotline.AccessNewsDeleteFldr) {
1171 return cc.NewErrReply(t, "You are not allowed to delete news folders.")
1172 }
1173 }
1174
1175 err = cc.Server.ThreadedNewsMgr.DeleteNewsItem(pathStrs)
1176 if err != nil {
1177 return res
1178 }
1179
1180 return append(res, cc.NewReply(t))
1181}
1182
1183// HandleDelNewsArt deletes a threaded news article.
1184// Request Fields
1185// 325 News path
1186// 326 News article Type
1187// 337 News article recursive delete - Delete child articles (1) or not (0)
1188func HandleDelNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1189 if !cc.Authorize(hotline.AccessNewsDeleteArt) {
1190 return cc.NewErrReply(t, "You are not allowed to delete news articles.")
1191
1192 }
1193
1194 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1195 if err != nil {
1196 return res
1197 }
1198
1199 articleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1200 if err != nil {
1201 cc.Logger.Error("error reading article Type", "err", err)
1202 return
1203 }
1204
1205 deleteRecursive := bytes.Equal([]byte{0, 1}, t.GetField(hotline.FieldNewsArtRecurseDel).Data)
1206
1207 err = cc.Server.ThreadedNewsMgr.DeleteArticle(pathStrs, uint32(articleID), deleteRecursive)
1208 if err != nil {
1209 cc.Logger.Error("error deleting news article", "err", err)
1210 }
1211
1212 return []hotline.Transaction{cc.NewReply(t)}
1213}
1214
1215// Request fields
1216// 325 News path
1217// 326 News article Type Type of the parent article?
1218// 328 News article title
1219// 334 News article flags
1220// 327 News article data flavor Currently “text/plain”
1221// 333 News article data
1222func HandlePostNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1223 if !cc.Authorize(hotline.AccessNewsPostArt) {
1224 return cc.NewErrReply(t, "You are not allowed to post news articles.")
1225 }
1226
1227 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
318eb6bc
JH
1228 if err != nil || len(pathStrs) == 0 {
1229 cc.Logger.Error("invalid news path")
fd740bc4
JH
1230 return res
1231 }
1232
1233 parentArticleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1234 if err != nil {
1235 return res
1236 }
1237
1238 err = cc.Server.ThreadedNewsMgr.PostArticle(
1239 pathStrs,
1240 uint32(parentArticleID),
1241 hotline.NewsArtData{
1242 Title: string(t.GetField(hotline.FieldNewsArtTitle).Data),
1243 Poster: string(cc.UserName),
1244 Date: hotline.NewTime(time.Now()),
1245 DataFlav: hotline.NewsFlavor,
1246 Data: string(t.GetField(hotline.FieldNewsArtData).Data),
1247 },
1248 )
1249 if err != nil {
1250 cc.Logger.Error("error posting news article", "err", err)
1251 }
1252
1253 return append(res, cc.NewReply(t))
1254}
1255
1256// HandleGetMsgs returns the flat news data
1257func HandleGetMsgs(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1258 if !cc.Authorize(hotline.AccessNewsReadArt) {
1259 return cc.NewErrReply(t, "You are not allowed to read news.")
1260 }
1261
1262 _, _ = cc.Server.MessageBoard.Seek(0, 0)
1263
1264 newsData, err := io.ReadAll(cc.Server.MessageBoard)
1265 if err != nil {
9f89cd9f 1266 cc.Logger.Error("Error reading messageboard", "err", err)
fd740bc4
JH
1267 }
1268
1269 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldData, newsData)))
1270}
1271
1272func HandleDownloadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1273 if !cc.Authorize(hotline.AccessDownloadFile) {
1274 return cc.NewErrReply(t, "You are not allowed to download files.")
1275 }
1276
1277 fileName := t.GetField(hotline.FieldFileName).Data
1278 filePath := t.GetField(hotline.FieldFilePath).Data
1279 resumeData := t.GetField(hotline.FieldFileResumeData).Data
1280
1281 var dataOffset int64
1282 var frd hotline.FileResumeData
1283 if resumeData != nil {
1284 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1285 return res
1286 }
1287 // TODO: handle rsrc fork offset
1288 dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
1289 }
1290
dcd23d53 1291 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1292 if err != nil {
1293 return res
1294 }
1295
1296 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
1297 if err != nil {
1298 return res
1299 }
1300
1301 xferSize := hlFile.Ffo.TransferSize(0)
1302
dcd23d53
JH
1303 ft := cc.NewFileTransfer(
1304 hotline.FileDownload,
1305 cc.FileRoot(),
1306 fileName,
1307 filePath,
1308 xferSize,
1309 )
fd740bc4 1310
fd740bc4
JH
1311 if resumeData != nil {
1312 var frd hotline.FileResumeData
1313 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1314 return res
1315 }
1316 ft.FileResumeData = &frd
1317 }
1318
1319 // Optional field for when a client requests file preview
1320 // Used only for TEXT, JPEG, GIFF, BMP or PICT files
1321 // The value will always be 2
1322 if t.GetField(hotline.FieldFileTransferOptions).Data != nil {
1323 ft.Options = t.GetField(hotline.FieldFileTransferOptions).Data
1324 xferSize = hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]
1325 }
1326
1327 res = append(res, cc.NewReply(t,
1328 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1329 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1330 hotline.NewField(hotline.FieldTransferSize, xferSize),
1331 hotline.NewField(hotline.FieldFileSize, hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]),
1332 ))
1333
1334 return res
1335}
1336
1337// Download all files from the specified folder and sub-folders
1338func HandleDownloadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
72f8a1fd 1339 if !cc.Authorize(hotline.AccessDownloadFolder) {
fd740bc4
JH
1340 return cc.NewErrReply(t, "You are not allowed to download folders.")
1341 }
1342
dcd23d53 1343 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
fd740bc4 1344 if err != nil {
dcd23d53 1345 return nil
fd740bc4
JH
1346 }
1347
1348 transferSize, err := hotline.CalcTotalSize(fullFilePath)
1349 if err != nil {
dcd23d53 1350 return nil
fd740bc4
JH
1351 }
1352 itemCount, err := hotline.CalcItemCount(fullFilePath)
1353 if err != nil {
dcd23d53 1354 return nil
fd740bc4
JH
1355 }
1356
dcd23d53 1357 fileTransfer := cc.NewFileTransfer(hotline.FolderDownload, cc.FileRoot(), t.GetField(hotline.FieldFileName).Data, t.GetField(hotline.FieldFilePath).Data, transferSize)
fd740bc4
JH
1358
1359 var fp hotline.FilePath
1360 _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data)
1361 if err != nil {
dcd23d53 1362 return nil
fd740bc4
JH
1363 }
1364
1365 res = append(res, cc.NewReply(t,
1366 hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:]),
1367 hotline.NewField(hotline.FieldTransferSize, transferSize),
1368 hotline.NewField(hotline.FieldFolderItemCount, itemCount),
1369 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1370 ))
1371 return res
1372}
1373
1374// Upload all files from the local folder and its subfolders to the specified path on the server
1375// Fields used in the request
1376// 201 File Name
1377// 202 File path
1378// 108 hotline.Transfer size Total size of all items in the folder
1379// 220 Folder item count
1380// 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1381func HandleUploadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
72f8a1fd
JH
1382 if !cc.Authorize(hotline.AccessUploadFolder) {
1383 return cc.NewErrReply(t, "You are not allowed to upload folders.")
1384 }
1385
fd740bc4
JH
1386 var fp hotline.FilePath
1387 if t.GetField(hotline.FieldFilePath).Data != nil {
1388 if _, err := fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1389 return res
1390 }
1391 }
1392
1393 // Handle special cases for Upload and Drop Box folders
1394 if !cc.Authorize(hotline.AccessUploadAnywhere) {
d34160c5 1395 if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() {
fd740bc4
JH
1396 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(hotline.FieldFileName).Data)))
1397 }
1398 }
1399
1400 fileTransfer := cc.NewFileTransfer(hotline.FolderUpload,
dcd23d53 1401 cc.FileRoot(),
fd740bc4
JH
1402 t.GetField(hotline.FieldFileName).Data,
1403 t.GetField(hotline.FieldFilePath).Data,
1404 t.GetField(hotline.FieldTransferSize).Data,
1405 )
1406
1407 fileTransfer.FolderItemCount = t.GetField(hotline.FieldFolderItemCount).Data
1408
1409 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:])))
1410}
1411
1412// HandleUploadFile
1413// Fields used in the request:
1414// 201 File Name
1415// 202 File path
1416// 204 File transfer options "Optional
1417// Used only to resume download, currently has value 2"
1418// 108 File transfer size "Optional used if download is not resumed"
1419func HandleUploadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1420 if !cc.Authorize(hotline.AccessUploadFile) {
1421 return cc.NewErrReply(t, "You are not allowed to upload files.")
1422 }
1423
1424 fileName := t.GetField(hotline.FieldFileName).Data
1425 filePath := t.GetField(hotline.FieldFilePath).Data
1426 transferOptions := t.GetField(hotline.FieldFileTransferOptions).Data
1427 transferSize := t.GetField(hotline.FieldTransferSize).Data // not sent for resume
1428
1429 var fp hotline.FilePath
1430 if filePath != nil {
1431 if _, err := fp.Write(filePath); err != nil {
1432 return res
1433 }
1434 }
1435
1436 // Handle special cases for Upload and Drop Box folders
1437 if !cc.Authorize(hotline.AccessUploadAnywhere) {
d34160c5 1438 if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() {
fd740bc4
JH
1439 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName)))
1440 }
1441 }
dcd23d53 1442 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1443 if err != nil {
1444 return res
1445 }
1446
1447 if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
8cd28eb8
RBR
1448 if !fp.IsUserDir() {
1449 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different Name.", string(fileName)))
1450 }
fd740bc4
JH
1451 }
1452
dcd23d53 1453 ft := cc.NewFileTransfer(hotline.FileUpload, cc.FileRoot(), fileName, filePath, transferSize)
fd740bc4
JH
1454
1455 replyT := cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]))
1456
1457 // client has requested to resume a partially transferred file
1458 if transferOptions != nil {
1459 fileInfo, err := cc.Server.FS.Stat(fullFilePath + hotline.IncompleteFileSuffix)
1460 if err != nil {
1461 return res
1462 }
1463
1464 offset := make([]byte, 4)
1465 binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
1466
1467 fileResumeData := hotline.NewFileResumeData([]hotline.ForkInfoList{
1468 *hotline.NewForkInfoList(offset),
1469 })
1470
1471 b, _ := fileResumeData.BinaryMarshal()
1472
1473 ft.TransferSize = offset
1474
1475 replyT.Fields = append(replyT.Fields, hotline.NewField(hotline.FieldFileResumeData, b))
1476 }
1477
1478 res = append(res, replyT)
1479 return res
1480}
1481
1482func HandleSetClientUserInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1483 if len(t.GetField(hotline.FieldUserIconID).Data) == 4 {
1484 cc.Icon = t.GetField(hotline.FieldUserIconID).Data[2:]
1485 } else {
1486 cc.Icon = t.GetField(hotline.FieldUserIconID).Data
1487 }
1488 if cc.Authorize(hotline.AccessAnyName) {
1489 cc.UserName = t.GetField(hotline.FieldUserName).Data
1490 }
1491
1492 // the options field is only passed by the client versions > 1.2.3.
1493 options := t.GetField(hotline.FieldOptions).Data
1494 if options != nil {
1495 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1496
1497 cc.Flags.Set(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM))
1498 cc.Flags.Set(hotline.UserFlagRefusePChat, optBitmap.Bit(hotline.UserOptRefuseChat))
1499
1500 // Check auto response
1501 if optBitmap.Bit(hotline.UserOptAutoResponse) == 1 {
1502 cc.AutoReply = t.GetField(hotline.FieldAutomaticResponse).Data
1503 } else {
1504 cc.AutoReply = []byte{}
1505 }
1506 }
1507
1508 for _, c := range cc.Server.ClientMgr.List() {
1509 res = append(res, hotline.NewTransaction(
1510 hotline.TranNotifyChangeUser,
1511 c.ID,
1512 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1513 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1514 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1515 hotline.NewField(hotline.FieldUserName, cc.UserName),
1516 ))
1517 }
1518
1519 return res
1520}
1521
1522// HandleKeepAlive responds to keepalive transactions with an empty reply
1523// * HL 1.9.2 Client sends keepalive msg every 3 minutes
1524// * HL 1.2.3 Client doesn't send keepalives
1525func HandleKeepAlive(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1526 res = append(res, cc.NewReply(t))
1527
1528 return res
1529}
1530
1531func HandleGetFileNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1532 fullPath, err := hotline.ReadPath(
dcd23d53 1533 cc.FileRoot(),
fd740bc4
JH
1534 t.GetField(hotline.FieldFilePath).Data,
1535 nil,
1536 )
1537 if err != nil {
1538 return res
1539 }
1540
1541 var fp hotline.FilePath
1542 if t.GetField(hotline.FieldFilePath).Data != nil {
1543 if _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1544 return res
1545 }
1546 }
1547
1548 // Handle special case for drop box folders
1549 if fp.IsDropbox() && !cc.Authorize(hotline.AccessViewDropBoxes) {
1550 return cc.NewErrReply(t, "You are not allowed to view drop boxes.")
1551 }
1552
1553 fileNames, err := hotline.GetFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
1554 if err != nil {
1555 return res
1556 }
1557
1558 res = append(res, cc.NewReply(t, fileNames...))
1559
1560 return res
1561}
1562
1563// =================================
1564// Hotline private chat flow
1565// =================================
1566// 1. ClientA sends TranInviteNewChat to server with user Type to invite
1567// 2. Server creates new ChatID
1568// 3. Server sends TranInviteToChat to invitee
1569// 4. Server replies to ClientA with new Chat Type
1570//
1571// A dialog box pops up in the invitee client with options to accept or decline the invitation.
1572// If Accepted is clicked:
1573// 1. ClientB sends TranJoinChat with FieldChatID
1574
1575// HandleInviteNewChat invites users to new private chat
1576func HandleInviteNewChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1577 if !cc.Authorize(hotline.AccessOpenChat) {
1578 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1579 }
1580
1581 // Client to Invite
1582 targetID := t.GetField(hotline.FieldUserID).Data
1583
1584 // Create a new chat with self as initial member.
1585 newChatID := cc.Server.ChatMgr.New(cc)
1586
1587 // Check if target user has "Refuse private chat" flag
1588 targetClient := cc.Server.ClientMgr.Get([2]byte(targetID))
1589 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags[:])))
1590 if flagBitmap.Bit(hotline.UserFlagRefusePChat) == 1 {
1591 res = append(res,
1592 hotline.NewTransaction(
1593 hotline.TranServerMsg,
1594 cc.ID,
1595 hotline.NewField(hotline.FieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
1596 hotline.NewField(hotline.FieldUserName, targetClient.UserName),
1597 hotline.NewField(hotline.FieldUserID, targetClient.ID[:]),
1598 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
1599 ),
1600 )
1601 } else {
1602 res = append(res,
1603 hotline.NewTransaction(
1604 hotline.TranInviteToChat,
1605 [2]byte(targetID),
1606 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1607 hotline.NewField(hotline.FieldUserName, cc.UserName),
1608 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1609 ),
1610 )
1611 }
1612
1613 return append(
1614 res,
1615 cc.NewReply(t,
1616 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1617 hotline.NewField(hotline.FieldUserName, cc.UserName),
1618 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1619 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1620 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1621 ),
1622 )
1623}
1624
1625func HandleInviteToChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1626 if !cc.Authorize(hotline.AccessOpenChat) {
1627 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1628 }
1629
1630 // Client to Invite
1631 targetID := t.GetField(hotline.FieldUserID).Data
1632 chatID := t.GetField(hotline.FieldChatID).Data
1633
1634 return []hotline.Transaction{
1635 hotline.NewTransaction(
1636 hotline.TranInviteToChat,
1637 [2]byte(targetID),
1638 hotline.NewField(hotline.FieldChatID, chatID),
1639 hotline.NewField(hotline.FieldUserName, cc.UserName),
1640 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1641 ),
1642 cc.NewReply(
1643 t,
1644 hotline.NewField(hotline.FieldChatID, chatID),
1645 hotline.NewField(hotline.FieldUserName, cc.UserName),
1646 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1647 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1648 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1649 ),
1650 }
1651}
1652
1653func HandleRejectChatInvite(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1654 chatID := [4]byte(t.GetField(hotline.FieldChatID).Data)
1655
1656 for _, c := range cc.Server.ChatMgr.Members(chatID) {
1657 res = append(res,
1658 hotline.NewTransaction(
1659 hotline.TranChatMsg,
1660 c.ID,
1661 hotline.NewField(hotline.FieldChatID, chatID[:]),
1662 hotline.NewField(hotline.FieldData, append(cc.UserName, []byte(" declined invitation to chat")...)),
1663 ),
1664 )
1665 }
1666
1667 return res
1668}
1669
1670// HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1671// Fields used in the reply:
1672// * 115 Chat subject
1673// * 300 User Name with info (Optional)
1674// * 300 (more user names with info)
1675func HandleJoinChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1676 chatID := t.GetField(hotline.FieldChatID).Data
1677
1678 // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
1679 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1680 res = append(res,
1681 hotline.NewTransaction(
1682 hotline.TranNotifyChatChangeUser,
1683 c.ID,
1684 hotline.NewField(hotline.FieldChatID, chatID),
1685 hotline.NewField(hotline.FieldUserName, cc.UserName),
1686 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1687 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1688 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1689 ),
1690 )
1691 }
1692
1693 cc.Server.ChatMgr.Join(hotline.ChatID(chatID), cc)
1694
1695 subject := cc.Server.ChatMgr.GetSubject(hotline.ChatID(chatID))
1696
1697 replyFields := []hotline.Field{hotline.NewField(hotline.FieldChatSubject, []byte(subject))}
1698 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1699 b, err := io.ReadAll(&hotline.User{
1700 ID: c.ID,
1701 Icon: c.Icon,
1702 Flags: c.Flags[:],
1703 Name: string(c.UserName),
1704 })
1705 if err != nil {
1706 return res
1707 }
1708 replyFields = append(replyFields, hotline.NewField(hotline.FieldUsernameWithInfo, b))
1709 }
1710
1711 return append(res, cc.NewReply(t, replyFields...))
1712}
1713
1714// HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1715// Fields used in the request:
1716// - 114 FieldChatID
1717//
1718// Reply is not expected.
1719func HandleLeaveChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1720 chatID := t.GetField(hotline.FieldChatID).Data
1721
1722 cc.Server.ChatMgr.Leave([4]byte(chatID), cc.ID)
1723
1724 // Notify members of the private chat that the user has left
1725 for _, c := range cc.Server.ChatMgr.Members(hotline.ChatID(chatID)) {
1726 res = append(res,
1727 hotline.NewTransaction(
1728 hotline.TranNotifyChatDeleteUser,
1729 c.ID,
1730 hotline.NewField(hotline.FieldChatID, chatID),
1731 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1732 ),
1733 )
1734 }
1735
1736 return res
1737}
1738
1739// HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1740// Fields used in the request:
1741// * 114 Chat Type
1742// * 115 Chat subject
1743// Reply is not expected.
1744func HandleSetChatSubject(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1745 chatID := t.GetField(hotline.FieldChatID).Data
1746
1747 cc.Server.ChatMgr.SetSubject([4]byte(chatID), string(t.GetField(hotline.FieldChatSubject).Data))
1748
1749 // Notify chat members of new subject.
1750 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1751 res = append(res,
1752 hotline.NewTransaction(
1753 hotline.TranNotifyChatSubject,
1754 c.ID,
1755 hotline.NewField(hotline.FieldChatID, chatID),
1756 hotline.NewField(hotline.FieldChatSubject, t.GetField(hotline.FieldChatSubject).Data),
1757 ),
1758 )
1759 }
1760
1761 return res
1762}
1763
1764// HandleMakeAlias makes a file alias using the specified path.
1765// Fields used in the request:
1766// 201 File Name
1767// 202 File path
1768// 212 File new path Destination path
1769//
1770// Fields used in the reply:
1771// None
1772func HandleMakeAlias(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1773 if !cc.Authorize(hotline.AccessMakeAlias) {
1774 return cc.NewErrReply(t, "You are not allowed to make aliases.")
1775 }
1776 fileName := t.GetField(hotline.FieldFileName).Data
1777 filePath := t.GetField(hotline.FieldFilePath).Data
1778 fileNewPath := t.GetField(hotline.FieldFileNewPath).Data
1779
dcd23d53 1780 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1781 if err != nil {
1782 return res
1783 }
1784
dcd23d53 1785 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), fileNewPath, fileName)
fd740bc4
JH
1786 if err != nil {
1787 return res
1788 }
1789
fd740bc4
JH
1790 if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1791 return cc.NewErrReply(t, "Error creating alias")
1792 }
1793
1794 res = append(res, cc.NewReply(t))
1795 return res
1796}
1797
1798// HandleDownloadBanner handles requests for a new banner from the server
1799// Fields used in the request:
1800// None
1801// Fields used in the reply:
1802// 107 FieldRefNum Used later for transfer
1803// 108 FieldTransferSize Size of data to be downloaded
1804func HandleDownloadBanner(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
dcd23d53 1805 ft := cc.NewFileTransfer(hotline.BannerDownload, "", []byte{}, []byte{}, make([]byte, 4))
fd740bc4
JH
1806 binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.Banner)))
1807
1808 return append(res, cc.NewReply(t,
1809 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1810 hotline.NewField(hotline.FieldTransferSize, ft.TransferSize),
1811 ))
1812}