]> git.r.bdr.sh - rbdr/mobius/blob - hotline/transaction_handlers.go
Fix multiple issues with v1.8+ login sequence
[rbdr/mobius] / hotline / transaction_handlers.go
1 package hotline
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "gopkg.in/yaml.v2"
9 "io/ioutil"
10 "math/big"
11 "os"
12 "path"
13 "sort"
14 "strings"
15 "time"
16 )
17
18 type TransactionType struct {
19 Access int // Specifies access privilege required to perform the transaction
20 DenyMsg string // The error reply message when user does not have access
21 Handler func(*ClientConn, *Transaction) ([]Transaction, error) // function for handling the transaction type
22 Name string // Name of transaction as it will appear in logging
23 RequiredFields []requiredField
24 }
25
26 var TransactionHandlers = map[uint16]TransactionType{
27 // Server initiated
28 tranChatMsg: {
29 Name: "tranChatMsg",
30 },
31 // Server initiated
32 tranNotifyChangeUser: {
33 Name: "tranNotifyChangeUser",
34 },
35 tranError: {
36 Name: "tranError",
37 },
38 tranShowAgreement: {
39 Name: "tranShowAgreement",
40 },
41 tranUserAccess: {
42 Name: "tranUserAccess",
43 },
44 tranNotifyDeleteUser: {
45 Name: "tranNotifyDeleteUser",
46 },
47 tranAgreed: {
48 Access: accessAlwaysAllow,
49 Name: "tranAgreed",
50 Handler: HandleTranAgreed,
51 },
52 tranChatSend: {
53 Access: accessSendChat,
54 DenyMsg: "You are not allowed to participate in chat.",
55 Handler: HandleChatSend,
56 Name: "tranChatSend",
57 RequiredFields: []requiredField{
58 {
59 ID: fieldData,
60 minLen: 0,
61 },
62 },
63 },
64 tranDelNewsArt: {
65 Access: accessNewsDeleteArt,
66 DenyMsg: "You are not allowed to delete news articles.",
67 Name: "tranDelNewsArt",
68 Handler: HandleDelNewsArt,
69 },
70 tranDelNewsItem: {
71 Access: accessAlwaysAllow, // Granular access enforced inside the handler
72 // Has multiple access flags: News Delete Folder (37) or News Delete Category (35)
73 // TODO: Implement inside the handler
74 Name: "tranDelNewsItem",
75 Handler: HandleDelNewsItem,
76 },
77 tranDeleteFile: {
78 Access: accessAlwaysAllow, // Granular access enforced inside the handler
79 Name: "tranDeleteFile",
80 Handler: HandleDeleteFile,
81 },
82 tranDeleteUser: {
83 Access: accessDeleteUser,
84 DenyMsg: "You are not allowed to delete accounts.",
85 Name: "tranDeleteUser",
86 Handler: HandleDeleteUser,
87 },
88 tranDisconnectUser: {
89 Access: accessDisconUser,
90 DenyMsg: "You are not allowed to disconnect users.",
91 Name: "tranDisconnectUser",
92 Handler: HandleDisconnectUser,
93 },
94 tranDownloadFile: {
95 Access: accessDownloadFile,
96 DenyMsg: "You are not allowed to download files.",
97 Name: "tranDownloadFile",
98 Handler: HandleDownloadFile,
99 },
100 tranDownloadFldr: {
101 Access: accessDownloadFile, // There is no specific access flag for folder vs file download
102 DenyMsg: "You are not allowed to download files.",
103 Name: "tranDownloadFldr",
104 Handler: HandleDownloadFolder,
105 },
106 tranGetClientInfoText: {
107 Access: accessGetClientInfo,
108 DenyMsg: "You are not allowed to get client info",
109 Name: "tranGetClientInfoText",
110 Handler: HandleGetClientConnInfoText,
111 },
112 tranGetFileInfo: {
113 Access: accessAlwaysAllow,
114 Name: "tranGetFileInfo",
115 Handler: HandleGetFileInfo,
116 },
117 tranGetFileNameList: {
118 Access: accessAlwaysAllow,
119 Name: "tranGetFileNameList",
120 Handler: HandleGetFileNameList,
121 },
122 tranGetMsgs: {
123 Access: accessNewsReadArt,
124 DenyMsg: "You are not allowed to read news.",
125 Name: "tranGetMsgs",
126 Handler: HandleGetMsgs,
127 },
128 tranGetNewsArtData: {
129 Access: accessNewsReadArt,
130 DenyMsg: "You are not allowed to read news.",
131 Name: "tranGetNewsArtData",
132 Handler: HandleGetNewsArtData,
133 },
134 tranGetNewsArtNameList: {
135 Access: accessNewsReadArt,
136 DenyMsg: "You are not allowed to read news.",
137 Name: "tranGetNewsArtNameList",
138 Handler: HandleGetNewsArtNameList,
139 },
140 tranGetNewsCatNameList: {
141 Access: accessNewsReadArt,
142 DenyMsg: "You are not allowed to read news.",
143 Name: "tranGetNewsCatNameList",
144 Handler: HandleGetNewsCatNameList,
145 },
146 tranGetUser: {
147 Access: accessOpenUser,
148 DenyMsg: "You are not allowed to view accounts.",
149 Name: "tranGetUser",
150 Handler: HandleGetUser,
151 },
152 tranGetUserNameList: {
153 Access: accessAlwaysAllow,
154 Name: "tranHandleGetUserNameList",
155 Handler: HandleGetUserNameList,
156 },
157 tranInviteNewChat: {
158 Access: accessOpenChat,
159 DenyMsg: "You are not allowed to request private chat.",
160 Name: "tranInviteNewChat",
161 Handler: HandleInviteNewChat,
162 },
163 tranInviteToChat: {
164 Access: accessOpenChat,
165 DenyMsg: "You are not allowed to request private chat.",
166 Name: "tranInviteToChat",
167 Handler: HandleInviteToChat,
168 },
169 tranJoinChat: {
170 Access: accessAlwaysAllow,
171 Name: "tranJoinChat",
172 Handler: HandleJoinChat,
173 },
174 tranKeepAlive: {
175 Access: accessAlwaysAllow,
176 Name: "tranKeepAlive",
177 Handler: HandleKeepAlive,
178 },
179 tranLeaveChat: {
180 Access: accessAlwaysAllow,
181 Name: "tranJoinChat",
182 Handler: HandleLeaveChat,
183 },
184
185 tranListUsers: {
186 Access: accessOpenUser,
187 DenyMsg: "You are not allowed to view accounts.",
188 Name: "tranListUsers",
189 Handler: HandleListUsers,
190 },
191 tranMoveFile: {
192 Access: accessMoveFile,
193 DenyMsg: "You are not allowed to move files.",
194 Name: "tranMoveFile",
195 Handler: HandleMoveFile,
196 },
197 tranNewFolder: {
198 Access: accessCreateFolder,
199 DenyMsg: "You are not allow to create folders.",
200 Name: "tranNewFolder",
201 Handler: HandleNewFolder,
202 },
203 tranNewNewsCat: {
204 Access: accessNewsCreateCat,
205 DenyMsg: "You are not allowed to create news categories.",
206 Name: "tranNewNewsCat",
207 Handler: HandleNewNewsCat,
208 },
209 tranNewNewsFldr: {
210 Access: accessNewsCreateFldr,
211 DenyMsg: "You are not allowed to create news folders.",
212 Name: "tranNewNewsFldr",
213 Handler: HandleNewNewsFldr,
214 },
215 tranNewUser: {
216 Access: accessCreateUser,
217 DenyMsg: "You are not allowed to create new accounts.",
218 Name: "tranNewUser",
219 Handler: HandleNewUser,
220 },
221 tranOldPostNews: {
222 Access: accessNewsPostArt,
223 DenyMsg: "You are not allowed to post news.",
224 Name: "tranOldPostNews",
225 Handler: HandleTranOldPostNews,
226 },
227 tranPostNewsArt: {
228 Access: accessNewsPostArt,
229 DenyMsg: "You are not allowed to post news articles.",
230 Name: "tranPostNewsArt",
231 Handler: HandlePostNewsArt,
232 },
233 tranRejectChatInvite: {
234 Access: accessAlwaysAllow,
235 Name: "tranRejectChatInvite",
236 Handler: HandleRejectChatInvite,
237 },
238 tranSendInstantMsg: {
239 Access: accessAlwaysAllow,
240 //Access: accessSendPrivMsg,
241 //DenyMsg: "You are not allowed to send private messages",
242 Name: "tranSendInstantMsg",
243 Handler: HandleSendInstantMsg,
244 RequiredFields: []requiredField{
245 {
246 ID: fieldData,
247 minLen: 0,
248 },
249 {
250 ID: fieldUserID,
251 },
252 },
253 },
254 tranSetChatSubject: {
255 Access: accessAlwaysAllow,
256 Name: "tranSetChatSubject",
257 Handler: HandleSetChatSubject,
258 },
259 tranSetClientUserInfo: {
260 Access: accessAlwaysAllow,
261 Name: "tranSetClientUserInfo",
262 Handler: HandleSetClientUserInfo,
263 },
264 tranSetFileInfo: {
265 Access: accessAlwaysAllow, // granular access is in the handler
266 Name: "tranSetFileInfo",
267 Handler: HandleSetFileInfo,
268 },
269 tranSetUser: {
270 Access: accessModifyUser,
271 DenyMsg: "You are not allowed to modify accounts.",
272 Name: "tranSetUser",
273 Handler: HandleSetUser,
274 },
275 tranUploadFile: {
276 Access: accessAlwaysAllow,
277 DenyMsg: "You are not allowed to upload files.",
278 Name: "tranUploadFile",
279 Handler: HandleUploadFile,
280 },
281 tranUploadFldr: {
282 Access: accessAlwaysAllow, // TODO: what should this be?
283 Name: "tranUploadFldr",
284 Handler: HandleUploadFolder,
285 },
286 tranUserBroadcast: {
287 Access: accessBroadcast,
288 DenyMsg: "You are not allowed to send broadcast messages.",
289 Name: "tranUserBroadcast",
290 Handler: HandleUserBroadcast,
291 },
292 }
293
294 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
295 // Truncate long usernames
296 trunc := fmt.Sprintf("%13s", cc.UserName)
297 formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(fieldData).Data)
298
299 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
300 // *** Halcyon does stuff
301 // This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
302 if t.GetField(fieldChatOptions).Data != nil {
303 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
304 }
305
306 if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
307 formattedMsg = strings.Replace(cc.Server.Stats.String(), "\n", "\r", -1)
308 }
309
310 chatID := t.GetField(fieldChatID).Data
311 // a non-nil chatID indicates the message belongs to a private chat
312 if chatID != nil {
313 chatInt := binary.BigEndian.Uint32(chatID)
314 privChat := cc.Server.PrivateChats[chatInt]
315
316 // send the message to all connected clients of the private chat
317 for _, c := range privChat.ClientConn {
318 res = append(res, *NewTransaction(
319 tranChatMsg,
320 c.ID,
321 NewField(fieldChatID, chatID),
322 NewField(fieldData, []byte(formattedMsg)),
323 ))
324 }
325 return res, err
326 }
327
328 for _, c := range sortedClients(cc.Server.Clients) {
329 // Filter out clients that do not have the read chat permission
330 if authorize(c.Account.Access, accessReadChat) {
331 res = append(res, *NewTransaction(tranChatMsg, c.ID, NewField(fieldData, []byte(formattedMsg))))
332 }
333 }
334
335 return res, err
336 }
337
338 // HandleSendInstantMsg sends instant message to the user on the current server.
339 // Fields used in the request:
340 // 103 User ID
341 // 113 Options
342 // One of the following values:
343 // - User message (myOpt_UserMessage = 1)
344 // - Refuse message (myOpt_RefuseMessage = 2)
345 // - Refuse chat (myOpt_RefuseChat = 3)
346 // - Automatic response (myOpt_AutomaticResponse = 4)"
347 // 101 Data Optional
348 // 214 Quoting message Optional
349 //
350 //Fields used in the reply:
351 // None
352 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
353 msg := t.GetField(fieldData)
354 ID := t.GetField(fieldUserID)
355 // TODO: Implement reply quoting
356 //options := transaction.GetField(hotline.fieldOptions)
357
358 res = append(res,
359 *NewTransaction(
360 tranServerMsg,
361 &ID.Data,
362 NewField(fieldData, msg.Data),
363 NewField(fieldUserName, cc.UserName),
364 NewField(fieldUserID, *cc.ID),
365 NewField(fieldOptions, []byte{0, 1}),
366 ),
367 )
368 id, _ := byteToInt(ID.Data)
369
370 //keys := make([]uint16, 0, len(cc.Server.Clients))
371 //for k := range cc.Server.Clients {
372 // keys = append(keys, k)
373 //}
374
375 otherClient := cc.Server.Clients[uint16(id)]
376 if otherClient == nil {
377 return res, errors.New("ohno")
378 }
379
380 // Respond with auto reply if other client has it enabled
381 if len(*otherClient.AutoReply) > 0 {
382 res = append(res,
383 *NewTransaction(
384 tranServerMsg,
385 cc.ID,
386 NewField(fieldData, *otherClient.AutoReply),
387 NewField(fieldUserName, otherClient.UserName),
388 NewField(fieldUserID, *otherClient.ID),
389 NewField(fieldOptions, []byte{0, 1}),
390 ),
391 )
392 }
393
394 res = append(res, cc.NewReply(t))
395
396 return res, err
397 }
398
399 func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
400 fileName := t.GetField(fieldFileName).Data
401 filePath := t.GetField(fieldFilePath).Data
402
403 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
404 if err != nil {
405 return res, err
406 }
407
408 res = append(res, cc.NewReply(t,
409 NewField(fieldFileName, fileName),
410 NewField(fieldFileTypeString, ffo.FlatFileInformationFork.TypeSignature),
411 NewField(fieldFileCreatorString, ffo.FlatFileInformationFork.CreatorSignature),
412 NewField(fieldFileComment, ffo.FlatFileInformationFork.Comment),
413 NewField(fieldFileType, ffo.FlatFileInformationFork.TypeSignature),
414 NewField(fieldFileCreateDate, ffo.FlatFileInformationFork.CreateDate),
415 NewField(fieldFileModifyDate, ffo.FlatFileInformationFork.ModifyDate),
416 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
417 ))
418 return res, err
419 }
420
421 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
422 // TODO: Implement support for comments
423 // Fields used in the request:
424 // * 201 File name
425 // * 202 File path Optional
426 // * 211 File new name Optional
427 // * 210 File comment Optional
428 // Fields used in the reply: None
429 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
430 fileName := t.GetField(fieldFileName).Data
431 filePath := t.GetField(fieldFilePath).Data
432
433 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
434 if err != nil {
435 return res, err
436 }
437
438 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(fieldFileNewName).Data)
439 if err != nil {
440 return nil, err
441 }
442
443 //fileComment := t.GetField(fieldFileComment).Data
444 fileNewName := t.GetField(fieldFileNewName).Data
445
446 if fileNewName != nil {
447 fi, err := FS.Stat(fullFilePath)
448 if err != nil {
449 return res, err
450 }
451 switch mode := fi.Mode(); {
452 case mode.IsDir():
453 if !authorize(cc.Account.Access, accessRenameFolder) {
454 res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
455 return res, err
456 }
457 case mode.IsRegular():
458 if !authorize(cc.Account.Access, accessRenameFile) {
459 res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
460 return res, err
461 }
462 }
463
464 err = os.Rename(fullFilePath, fullNewFilePath)
465 if os.IsNotExist(err) {
466 res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
467 return res, err
468 }
469 }
470
471 res = append(res, cc.NewReply(t))
472 return res, err
473 }
474
475 // HandleDeleteFile deletes a file or folder
476 // Fields used in the request:
477 // * 201 File name
478 // * 202 File path
479 // Fields used in the reply: none
480 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
481 fileName := t.GetField(fieldFileName).Data
482 filePath := t.GetField(fieldFilePath).Data
483
484 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
485 if err != nil {
486 return res, err
487 }
488
489 cc.Server.Logger.Debugw("Delete file", "src", fullFilePath)
490
491 fi, err := os.Stat(fullFilePath)
492 if err != nil {
493 res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
494 return res, nil
495 }
496 switch mode := fi.Mode(); {
497 case mode.IsDir():
498 if !authorize(cc.Account.Access, accessDeleteFolder) {
499 res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
500 return res, err
501 }
502 case mode.IsRegular():
503 if !authorize(cc.Account.Access, accessDeleteFile) {
504 res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
505 return res, err
506 }
507 }
508
509 if err := os.RemoveAll(fullFilePath); err != nil {
510 return res, err
511 }
512
513 res = append(res, cc.NewReply(t))
514 return res, err
515 }
516
517 // HandleMoveFile moves files or folders. Note: seemingly not documented
518 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
519 fileName := string(t.GetField(fieldFileName).Data)
520 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
521 fileNewPath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
522
523 cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
524
525 path := filePath + "/" + fileName
526 fi, err := os.Stat(path)
527 if err != nil {
528 return res, err
529 }
530 switch mode := fi.Mode(); {
531 case mode.IsDir():
532 if !authorize(cc.Account.Access, accessMoveFolder) {
533 res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
534 return res, err
535 }
536 case mode.IsRegular():
537 if !authorize(cc.Account.Access, accessMoveFile) {
538 res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
539 return res, err
540 }
541 }
542
543 err = os.Rename(filePath+"/"+fileName, fileNewPath+"/"+fileName)
544 if os.IsNotExist(err) {
545 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
546 return res, err
547 }
548 if err != nil {
549 return []Transaction{}, err
550 }
551 // TODO: handle other possible errors; e.g. file delete fails due to file permission issue
552
553 res = append(res, cc.NewReply(t))
554 return res, err
555 }
556
557 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
558 newFolderPath := cc.Server.Config.FileRoot
559 folderName := string(t.GetField(fieldFileName).Data)
560
561 folderName = path.Join("/", folderName)
562
563 // fieldFilePath is only present for nested paths
564 if t.GetField(fieldFilePath).Data != nil {
565 var newFp FilePath
566 err := newFp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
567 if err != nil {
568 return nil, err
569 }
570 newFolderPath += newFp.String()
571 }
572 newFolderPath = path.Join(newFolderPath, folderName)
573
574 // TODO: check path and folder name lengths
575
576 if _, err := FS.Stat(newFolderPath); !os.IsNotExist(err) {
577 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
578 return []Transaction{cc.NewErrReply(t, msg)}, nil
579 }
580
581 // TODO: check for disallowed characters to maintain compatibility for original client
582
583 if err := FS.Mkdir(newFolderPath, 0777); err != nil {
584 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
585 return []Transaction{cc.NewErrReply(t, msg)}, nil
586 }
587
588 res = append(res, cc.NewReply(t))
589 return res, err
590 }
591
592 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
593 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
594 userName := string(t.GetField(fieldUserName).Data)
595
596 newAccessLvl := t.GetField(fieldUserAccess).Data
597
598 account := cc.Server.Accounts[login]
599 account.Access = &newAccessLvl
600 account.Name = userName
601
602 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
603 // not include fieldUserPassword
604 if t.GetField(fieldUserPassword).Data == nil {
605 account.Password = hashAndSalt([]byte(""))
606 }
607 if len(t.GetField(fieldUserPassword).Data) > 1 {
608 account.Password = hashAndSalt(t.GetField(fieldUserPassword).Data)
609 }
610
611 file := cc.Server.ConfigDir + "Users/" + login + ".yaml"
612 out, err := yaml.Marshal(&account)
613 if err != nil {
614 return res, err
615 }
616 if err := ioutil.WriteFile(file, out, 0666); err != nil {
617 return res, err
618 }
619
620 // Notify connected clients logged in as the user of the new access level
621 for _, c := range cc.Server.Clients {
622 if c.Account.Login == login {
623 // Note: comment out these two lines to test server-side deny messages
624 newT := NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, newAccessLvl))
625 res = append(res, *newT)
626
627 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*c.Flags)))
628 if authorize(c.Account.Access, accessDisconUser) {
629 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
630 } else {
631 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
632 }
633 binary.BigEndian.PutUint16(*c.Flags, uint16(flagBitmap.Int64()))
634
635 c.Account.Access = account.Access
636
637 cc.sendAll(
638 tranNotifyChangeUser,
639 NewField(fieldUserID, *c.ID),
640 NewField(fieldUserFlags, *c.Flags),
641 NewField(fieldUserName, c.UserName),
642 NewField(fieldUserIconID, *c.Icon),
643 )
644 }
645 }
646
647 // TODO: If we have just promoted a connected user to admin, notify
648 // connected clients to turn the user red
649
650 res = append(res, cc.NewReply(t))
651 return res, err
652 }
653
654 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
655 userLogin := string(t.GetField(fieldUserLogin).Data)
656 account := cc.Server.Accounts[userLogin]
657 if account == nil {
658 errorT := cc.NewErrReply(t, "Account does not exist.")
659 res = append(res, errorT)
660 return res, err
661 }
662
663 res = append(res, cc.NewReply(t,
664 NewField(fieldUserName, []byte(account.Name)),
665 NewField(fieldUserLogin, negateString(t.GetField(fieldUserLogin).Data)),
666 NewField(fieldUserPassword, []byte(account.Password)),
667 NewField(fieldUserAccess, *account.Access),
668 ))
669 return res, err
670 }
671
672 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
673 var userFields []Field
674 // TODO: make order deterministic
675 for _, acc := range cc.Server.Accounts {
676 userField := acc.MarshalBinary()
677 userFields = append(userFields, NewField(fieldData, userField))
678 }
679
680 res = append(res, cc.NewReply(t, userFields...))
681 return res, err
682 }
683
684 // HandleNewUser creates a new user account
685 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
686 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
687
688 // If the account already exists, reply with an error
689 // TODO: make order deterministic
690 if _, ok := cc.Server.Accounts[login]; ok {
691 res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
692 return res, err
693 }
694
695 if err := cc.Server.NewUser(
696 login,
697 string(t.GetField(fieldUserName).Data),
698 string(t.GetField(fieldUserPassword).Data),
699 t.GetField(fieldUserAccess).Data,
700 ); err != nil {
701 return []Transaction{}, err
702 }
703
704 res = append(res, cc.NewReply(t))
705 return res, err
706 }
707
708 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
709 // TODO: Handle case where account doesn't exist; e.g. delete race condition
710 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
711
712 if err := cc.Server.DeleteUser(login); err != nil {
713 return res, err
714 }
715
716 res = append(res, cc.NewReply(t))
717 return res, err
718 }
719
720 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
721 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
722 cc.sendAll(
723 tranServerMsg,
724 NewField(fieldData, t.GetField(tranGetMsgs).Data),
725 NewField(fieldChatOptions, []byte{0}),
726 )
727
728 res = append(res, cc.NewReply(t))
729 return res, err
730 }
731
732 func byteToInt(bytes []byte) (int, error) {
733 switch len(bytes) {
734 case 2:
735 return int(binary.BigEndian.Uint16(bytes)), nil
736 case 4:
737 return int(binary.BigEndian.Uint32(bytes)), nil
738 }
739
740 return 0, errors.New("unknown byte length")
741 }
742
743 func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
744 clientID, _ := byteToInt(t.GetField(fieldUserID).Data)
745
746 clientConn := cc.Server.Clients[uint16(clientID)]
747 if clientConn == nil {
748 return res, errors.New("invalid client")
749 }
750
751 // TODO: Implement non-hardcoded values
752 template := `Nickname: %s
753 Name: %s
754 Account: %s
755 Address: %s
756
757 -------- File Downloads ---------
758
759 %s
760
761 ------- Folder Downloads --------
762
763 None.
764
765 --------- File Uploads ----------
766
767 None.
768
769 -------- Folder Uploads ---------
770
771 None.
772
773 ------- Waiting Downloads -------
774
775 None.
776
777 `
778
779 activeDownloads := clientConn.Transfers[FileDownload]
780 activeDownloadList := "None."
781 for _, dl := range activeDownloads {
782 activeDownloadList += dl.String() + "\n"
783 }
784
785 template = fmt.Sprintf(
786 template,
787 clientConn.UserName,
788 clientConn.Account.Name,
789 clientConn.Account.Login,
790 clientConn.Connection.RemoteAddr().String(),
791 activeDownloadList,
792 )
793 template = strings.Replace(template, "\n", "\r", -1)
794
795 res = append(res, cc.NewReply(t,
796 NewField(fieldData, []byte(template)),
797 NewField(fieldUserName, clientConn.UserName),
798 ))
799 return res, err
800 }
801
802 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
803 res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
804
805 return res, err
806 }
807
808 func (cc *ClientConn) notifyNewUserHasJoined() (res []Transaction, err error) {
809 // Notify other ccs that a new user has connected
810 cc.NotifyOthers(
811 *NewTransaction(
812 tranNotifyChangeUser, nil,
813 NewField(fieldUserName, cc.UserName),
814 NewField(fieldUserID, *cc.ID),
815 NewField(fieldUserIconID, *cc.Icon),
816 NewField(fieldUserFlags, *cc.Flags),
817 ),
818 )
819
820 return res, nil
821 }
822
823 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
824 cc.Agreed = true
825 cc.UserName = t.GetField(fieldUserName).Data
826 *cc.Icon = t.GetField(fieldUserIconID).Data
827
828 options := t.GetField(fieldOptions).Data
829 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
830
831 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
832
833 // Check refuse private PM option
834 if optBitmap.Bit(refusePM) == 1 {
835 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
836 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
837 }
838
839 // Check refuse private chat option
840 if optBitmap.Bit(refuseChat) == 1 {
841 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
842 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
843 }
844
845 // Check auto response
846 if optBitmap.Bit(autoResponse) == 1 {
847 *cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
848 } else {
849 *cc.AutoReply = []byte{}
850 }
851
852 _, _ = cc.notifyNewUserHasJoined()
853
854 res = append(res, cc.NewReply(t))
855
856 return res, err
857 }
858
859 const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
860 // "Mon, 02 Jan 2006 15:04:05 MST"
861
862 const defaultNewsTemplate = `From %s (%s):
863
864 %s
865
866 __________________________________________________________`
867
868 // HandleTranOldPostNews updates the flat news
869 // Fields used in this request:
870 // 101 Data
871 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
872 cc.Server.flatNewsMux.Lock()
873 defer cc.Server.flatNewsMux.Unlock()
874
875 newsDateTemplate := defaultNewsDateFormat
876 if cc.Server.Config.NewsDateFormat != "" {
877 newsDateTemplate = cc.Server.Config.NewsDateFormat
878 }
879
880 newsTemplate := defaultNewsTemplate
881 if cc.Server.Config.NewsDelimiter != "" {
882 newsTemplate = cc.Server.Config.NewsDelimiter
883 }
884
885 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
886 newsPost = strings.Replace(newsPost, "\n", "\r", -1)
887
888 // update news in memory
889 cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
890
891 // update news on disk
892 if err := ioutil.WriteFile(cc.Server.ConfigDir+"MessageBoard.txt", cc.Server.FlatNews, 0644); err != nil {
893 return res, err
894 }
895
896 // Notify all clients of updated news
897 cc.sendAll(
898 tranNewMsg,
899 NewField(fieldData, []byte(newsPost)),
900 )
901
902 res = append(res, cc.NewReply(t))
903 return res, err
904 }
905
906 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
907 clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(fieldUserID).Data)]
908
909 if authorize(clientConn.Account.Access, accessCannotBeDiscon) {
910 res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
911 return res, err
912 }
913
914 if err := clientConn.Connection.Close(); err != nil {
915 return res, err
916 }
917
918 res = append(res, cc.NewReply(t))
919 return res, err
920 }
921
922 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
923 // Fields used in the request:
924 // 325 News path (Optional)
925
926 newsPath := t.GetField(fieldNewsPath).Data
927 cc.Server.Logger.Infow("NewsPath: ", "np", string(newsPath))
928
929 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
930 cats := cc.Server.GetNewsCatByPath(pathStrs)
931
932 // To store the keys in slice in sorted order
933 keys := make([]string, len(cats))
934 i := 0
935 for k := range cats {
936 keys[i] = k
937 i++
938 }
939 sort.Strings(keys)
940
941 var fieldData []Field
942 for _, k := range keys {
943 cat := cats[k]
944 b, _ := cat.MarshalBinary()
945 fieldData = append(fieldData, NewField(
946 fieldNewsCatListData15,
947 b,
948 ))
949 }
950
951 res = append(res, cc.NewReply(t, fieldData...))
952 return res, err
953 }
954
955 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
956 name := string(t.GetField(fieldNewsCatName).Data)
957 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
958
959 cats := cc.Server.GetNewsCatByPath(pathStrs)
960 cats[name] = NewsCategoryListData15{
961 Name: name,
962 Type: []byte{0, 3},
963 Articles: map[uint32]*NewsArtData{},
964 SubCats: make(map[string]NewsCategoryListData15),
965 }
966
967 if err := cc.Server.writeThreadedNews(); err != nil {
968 return res, err
969 }
970 res = append(res, cc.NewReply(t))
971 return res, err
972 }
973
974 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
975 // Fields used in the request:
976 // 322 News category name
977 // 325 News path
978 name := string(t.GetField(fieldFileName).Data)
979 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
980
981 cc.Server.Logger.Infof("Creating new news folder %s", name)
982
983 cats := cc.Server.GetNewsCatByPath(pathStrs)
984 cats[name] = NewsCategoryListData15{
985 Name: name,
986 Type: []byte{0, 2},
987 Articles: map[uint32]*NewsArtData{},
988 SubCats: make(map[string]NewsCategoryListData15),
989 }
990 if err := cc.Server.writeThreadedNews(); err != nil {
991 return res, err
992 }
993 res = append(res, cc.NewReply(t))
994 return res, err
995 }
996
997 // Fields used in the request:
998 // 325 News path Optional
999 //
1000 // Reply fields:
1001 // 321 News article list data Optional
1002 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1003 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1004
1005 var cat NewsCategoryListData15
1006 cats := cc.Server.ThreadedNews.Categories
1007
1008 for _, path := range pathStrs {
1009 cat = cats[path]
1010 cats = cats[path].SubCats
1011 }
1012
1013 nald := cat.GetNewsArtListData()
1014
1015 res = append(res, cc.NewReply(t, NewField(fieldNewsArtListData, nald.Payload())))
1016 return res, err
1017 }
1018
1019 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1020 // Request fields
1021 // 325 News path
1022 // 326 News article ID
1023 // 327 News article data flavor
1024
1025 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1026
1027 var cat NewsCategoryListData15
1028 cats := cc.Server.ThreadedNews.Categories
1029
1030 for _, path := range pathStrs {
1031 cat = cats[path]
1032 cats = cats[path].SubCats
1033 }
1034 newsArtID := t.GetField(fieldNewsArtID).Data
1035
1036 convertedArtID := binary.BigEndian.Uint16(newsArtID)
1037
1038 art := cat.Articles[uint32(convertedArtID)]
1039 if art == nil {
1040 res = append(res, cc.NewReply(t))
1041 return res, err
1042 }
1043
1044 // Reply fields
1045 // 328 News article title
1046 // 329 News article poster
1047 // 330 News article date
1048 // 331 Previous article ID
1049 // 332 Next article ID
1050 // 335 Parent article ID
1051 // 336 First child article ID
1052 // 327 News article data flavor "Should be “text/plain”
1053 // 333 News article data Optional (if data flavor is “text/plain”)
1054
1055 res = append(res, cc.NewReply(t,
1056 NewField(fieldNewsArtTitle, []byte(art.Title)),
1057 NewField(fieldNewsArtPoster, []byte(art.Poster)),
1058 NewField(fieldNewsArtDate, art.Date),
1059 NewField(fieldNewsArtPrevArt, art.PrevArt),
1060 NewField(fieldNewsArtNextArt, art.NextArt),
1061 NewField(fieldNewsArtParentArt, art.ParentArt),
1062 NewField(fieldNewsArt1stChildArt, art.FirstChildArt),
1063 NewField(fieldNewsArtDataFlav, []byte("text/plain")),
1064 NewField(fieldNewsArtData, []byte(art.Data)),
1065 ))
1066 return res, err
1067 }
1068
1069 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1070 // Access: News Delete Folder (37) or News Delete Category (35)
1071
1072 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1073
1074 // TODO: determine if path is a Folder (Bundle) or Category and check for permission
1075
1076 cc.Server.Logger.Infof("DelNewsItem %v", pathStrs)
1077
1078 cats := cc.Server.ThreadedNews.Categories
1079
1080 delName := pathStrs[len(pathStrs)-1]
1081 if len(pathStrs) > 1 {
1082 for _, path := range pathStrs[0 : len(pathStrs)-1] {
1083 cats = cats[path].SubCats
1084 }
1085 }
1086
1087 delete(cats, delName)
1088
1089 err = cc.Server.writeThreadedNews()
1090 if err != nil {
1091 return res, err
1092 }
1093
1094 // Reply params: none
1095 res = append(res, cc.NewReply(t))
1096
1097 return res, err
1098 }
1099
1100 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1101 // Request Fields
1102 // 325 News path
1103 // 326 News article ID
1104 // 337 News article – recursive delete Delete child articles (1) or not (0)
1105 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1106 ID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1107
1108 // TODO: Delete recursive
1109 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1110
1111 catName := pathStrs[len(pathStrs)-1]
1112 cat := cats[catName]
1113
1114 delete(cat.Articles, uint32(ID))
1115
1116 cats[catName] = cat
1117 if err := cc.Server.writeThreadedNews(); err != nil {
1118 return res, err
1119 }
1120
1121 res = append(res, cc.NewReply(t))
1122 return res, err
1123 }
1124
1125 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1126 // Request fields
1127 // 325 News path
1128 // 326 News article ID ID of the parent article?
1129 // 328 News article title
1130 // 334 News article flags
1131 // 327 News article data flavor Currently “text/plain”
1132 // 333 News article data
1133
1134 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1135 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1136
1137 catName := pathStrs[len(pathStrs)-1]
1138 cat := cats[catName]
1139
1140 newArt := NewsArtData{
1141 Title: string(t.GetField(fieldNewsArtTitle).Data),
1142 Poster: string(cc.UserName),
1143 Date: NewsDate(),
1144 PrevArt: []byte{0, 0, 0, 0},
1145 NextArt: []byte{0, 0, 0, 0},
1146 ParentArt: append([]byte{0, 0}, t.GetField(fieldNewsArtID).Data...),
1147 FirstChildArt: []byte{0, 0, 0, 0},
1148 DataFlav: []byte("text/plain"),
1149 Data: string(t.GetField(fieldNewsArtData).Data),
1150 }
1151
1152 var keys []int
1153 for k := range cat.Articles {
1154 keys = append(keys, int(k))
1155 }
1156
1157 nextID := uint32(1)
1158 if len(keys) > 0 {
1159 sort.Ints(keys)
1160 prevID := uint32(keys[len(keys)-1])
1161 nextID = prevID + 1
1162
1163 binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
1164
1165 // Set next article ID
1166 binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
1167 }
1168
1169 // Update parent article with first child reply
1170 parentID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1171 if parentID != 0 {
1172 parentArt := cat.Articles[uint32(parentID)]
1173
1174 if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
1175 binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
1176 }
1177 }
1178
1179 cat.Articles[nextID] = &newArt
1180
1181 cats[catName] = cat
1182 if err := cc.Server.writeThreadedNews(); err != nil {
1183 return res, err
1184 }
1185
1186 res = append(res, cc.NewReply(t))
1187 return res, err
1188 }
1189
1190 // HandleGetMsgs returns the flat news data
1191 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1192 res = append(res, cc.NewReply(t, NewField(fieldData, cc.Server.FlatNews)))
1193
1194 return res, err
1195 }
1196
1197 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1198 fileName := t.GetField(fieldFileName).Data
1199 filePath := t.GetField(fieldFilePath).Data
1200
1201 var fp FilePath
1202 err = fp.UnmarshalBinary(filePath)
1203 if err != nil {
1204 return res, err
1205 }
1206
1207 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
1208 if err != nil {
1209 return res, err
1210 }
1211
1212 transactionRef := cc.Server.NewTransactionRef()
1213 data := binary.BigEndian.Uint32(transactionRef)
1214
1215 ft := &FileTransfer{
1216 FileName: fileName,
1217 FilePath: filePath,
1218 ReferenceNumber: transactionRef,
1219 Type: FileDownload,
1220 }
1221
1222 cc.Server.FileTransfers[data] = ft
1223 cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft)
1224
1225 res = append(res, cc.NewReply(t,
1226 NewField(fieldRefNum, transactionRef),
1227 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1228 NewField(fieldTransferSize, ffo.TransferSize()),
1229 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
1230 ))
1231
1232 return res, err
1233 }
1234
1235 // Download all files from the specified folder and sub-folders
1236 // response example
1237 //
1238 // 00
1239 // 01
1240 // 00 00
1241 // 00 00 00 11
1242 // 00 00 00 00
1243 // 00 00 00 18
1244 // 00 00 00 18
1245 //
1246 // 00 03
1247 //
1248 // 00 6c // transfer size
1249 // 00 04 // len
1250 // 00 0f d5 ae
1251 //
1252 // 00 dc // field Folder item count
1253 // 00 02 // len
1254 // 00 02
1255 //
1256 // 00 6b // ref number
1257 // 00 04 // len
1258 // 00 03 64 b1
1259 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1260 transactionRef := cc.Server.NewTransactionRef()
1261 data := binary.BigEndian.Uint32(transactionRef)
1262
1263 fileTransfer := &FileTransfer{
1264 FileName: t.GetField(fieldFileName).Data,
1265 FilePath: t.GetField(fieldFilePath).Data,
1266 ReferenceNumber: transactionRef,
1267 Type: FolderDownload,
1268 }
1269 cc.Server.FileTransfers[data] = fileTransfer
1270 cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer)
1271
1272 var fp FilePath
1273 err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
1274 if err != nil {
1275 return res, err
1276 }
1277
1278 fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
1279
1280 transferSize, err := CalcTotalSize(fullFilePath)
1281 if err != nil {
1282 return res, err
1283 }
1284 itemCount, err := CalcItemCount(fullFilePath)
1285 if err != nil {
1286 return res, err
1287 }
1288 res = append(res, cc.NewReply(t,
1289 NewField(fieldRefNum, transactionRef),
1290 NewField(fieldTransferSize, transferSize),
1291 NewField(fieldFolderItemCount, itemCount),
1292 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1293 ))
1294 return res, err
1295 }
1296
1297 // Upload all files from the local folder and its subfolders to the specified path on the server
1298 // Fields used in the request
1299 // 201 File name
1300 // 202 File path
1301 // 108 transfer size Total size of all items in the folder
1302 // 220 Folder item count
1303 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1304 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1305 transactionRef := cc.Server.NewTransactionRef()
1306 data := binary.BigEndian.Uint32(transactionRef)
1307
1308 fileTransfer := &FileTransfer{
1309 FileName: t.GetField(fieldFileName).Data,
1310 FilePath: t.GetField(fieldFilePath).Data,
1311 ReferenceNumber: transactionRef,
1312 Type: FolderUpload,
1313 FolderItemCount: t.GetField(fieldFolderItemCount).Data,
1314 TransferSize: t.GetField(fieldTransferSize).Data,
1315 }
1316 cc.Server.FileTransfers[data] = fileTransfer
1317
1318 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1319 return res, err
1320 }
1321
1322 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1323 // TODO: add permission handing for upload folders and drop boxes
1324 if !authorize(cc.Account.Access, accessUploadFile) {
1325 res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
1326 return res, err
1327 }
1328
1329 fileName := t.GetField(fieldFileName).Data
1330 filePath := t.GetField(fieldFilePath).Data
1331
1332 transactionRef := cc.Server.NewTransactionRef()
1333 data := binary.BigEndian.Uint32(transactionRef)
1334
1335 cc.Server.FileTransfers[data] = &FileTransfer{
1336 FileName: fileName,
1337 FilePath: filePath,
1338 ReferenceNumber: transactionRef,
1339 Type: FileUpload,
1340 }
1341
1342 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1343 return res, err
1344 }
1345
1346 // User options
1347 const (
1348 refusePM = 0
1349 refuseChat = 1
1350 autoResponse = 2
1351 )
1352
1353 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1354 var icon []byte
1355 if len(t.GetField(fieldUserIconID).Data) == 4 {
1356 icon = t.GetField(fieldUserIconID).Data[2:]
1357 } else {
1358 icon = t.GetField(fieldUserIconID).Data
1359 }
1360 *cc.Icon = icon
1361 cc.UserName = t.GetField(fieldUserName).Data
1362
1363 // the options field is only passed by the client versions > 1.2.3.
1364 options := t.GetField(fieldOptions).Data
1365
1366 if options != nil {
1367 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1368 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
1369
1370 // Check refuse private PM option
1371 if optBitmap.Bit(refusePM) == 1 {
1372 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
1373 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1374 }
1375
1376 // Check refuse private chat option
1377 if optBitmap.Bit(refuseChat) == 1 {
1378 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
1379 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1380 }
1381
1382 // Check auto response
1383 if optBitmap.Bit(autoResponse) == 1 {
1384 *cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
1385 } else {
1386 *cc.AutoReply = []byte{}
1387 }
1388 }
1389
1390 // Notify all clients of updated user info
1391 cc.sendAll(
1392 tranNotifyChangeUser,
1393 NewField(fieldUserID, *cc.ID),
1394 NewField(fieldUserIconID, *cc.Icon),
1395 NewField(fieldUserFlags, *cc.Flags),
1396 NewField(fieldUserName, cc.UserName),
1397 )
1398
1399 return res, err
1400 }
1401
1402 // HandleKeepAlive response to keepalive transactions with an empty reply
1403 // HL 1.9.2 Client sends keepalive msg every 3 minutes
1404 // HL 1.2.3 Client doesn't send keepalives
1405 func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1406 res = append(res, cc.NewReply(t))
1407
1408 return res, err
1409 }
1410
1411 func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1412 fullPath, err := readPath(
1413 cc.Server.Config.FileRoot,
1414 t.GetField(fieldFilePath).Data,
1415 nil,
1416 )
1417 if err != nil {
1418 return res, err
1419 }
1420
1421 fileNames, err := getFileNameList(fullPath)
1422 if err != nil {
1423 return res, err
1424 }
1425
1426 res = append(res, cc.NewReply(t, fileNames...))
1427
1428 return res, err
1429 }
1430
1431 // =================================
1432 // Hotline private chat flow
1433 // =================================
1434 // 1. ClientA sends tranInviteNewChat to server with user ID to invite
1435 // 2. Server creates new ChatID
1436 // 3. Server sends tranInviteToChat to invitee
1437 // 4. Server replies to ClientA with new Chat ID
1438 //
1439 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1440 // If Accepted is clicked:
1441 // 1. ClientB sends tranJoinChat with fieldChatID
1442
1443 // HandleInviteNewChat invites users to new private chat
1444 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1445 // Client to Invite
1446 targetID := t.GetField(fieldUserID).Data
1447 newChatID := cc.Server.NewPrivateChat(cc)
1448
1449 res = append(res,
1450 *NewTransaction(
1451 tranInviteToChat,
1452 &targetID,
1453 NewField(fieldChatID, newChatID),
1454 NewField(fieldUserName, cc.UserName),
1455 NewField(fieldUserID, *cc.ID),
1456 ),
1457 )
1458
1459 res = append(res,
1460 cc.NewReply(t,
1461 NewField(fieldChatID, newChatID),
1462 NewField(fieldUserName, cc.UserName),
1463 NewField(fieldUserID, *cc.ID),
1464 NewField(fieldUserIconID, *cc.Icon),
1465 NewField(fieldUserFlags, *cc.Flags),
1466 ),
1467 )
1468
1469 return res, err
1470 }
1471
1472 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1473 // Client to Invite
1474 targetID := t.GetField(fieldUserID).Data
1475 chatID := t.GetField(fieldChatID).Data
1476
1477 res = append(res,
1478 *NewTransaction(
1479 tranInviteToChat,
1480 &targetID,
1481 NewField(fieldChatID, chatID),
1482 NewField(fieldUserName, cc.UserName),
1483 NewField(fieldUserID, *cc.ID),
1484 ),
1485 )
1486 res = append(res,
1487 cc.NewReply(
1488 t,
1489 NewField(fieldChatID, chatID),
1490 NewField(fieldUserName, cc.UserName),
1491 NewField(fieldUserID, *cc.ID),
1492 NewField(fieldUserIconID, *cc.Icon),
1493 NewField(fieldUserFlags, *cc.Flags),
1494 ),
1495 )
1496
1497 return res, err
1498 }
1499
1500 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1501 chatID := t.GetField(fieldChatID).Data
1502 chatInt := binary.BigEndian.Uint32(chatID)
1503
1504 privChat := cc.Server.PrivateChats[chatInt]
1505
1506 resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
1507
1508 for _, c := range sortedClients(privChat.ClientConn) {
1509 res = append(res,
1510 *NewTransaction(
1511 tranChatMsg,
1512 c.ID,
1513 NewField(fieldChatID, chatID),
1514 NewField(fieldData, resMsg),
1515 ),
1516 )
1517 }
1518
1519 return res, err
1520 }
1521
1522 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1523 // Fields used in the reply:
1524 // * 115 Chat subject
1525 // * 300 User name with info (Optional)
1526 // * 300 (more user names with info)
1527 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1528 chatID := t.GetField(fieldChatID).Data
1529 chatInt := binary.BigEndian.Uint32(chatID)
1530
1531 privChat := cc.Server.PrivateChats[chatInt]
1532
1533 // Send tranNotifyChatChangeUser to current members of the chat to inform of new user
1534 for _, c := range sortedClients(privChat.ClientConn) {
1535 res = append(res,
1536 *NewTransaction(
1537 tranNotifyChatChangeUser,
1538 c.ID,
1539 NewField(fieldChatID, chatID),
1540 NewField(fieldUserName, cc.UserName),
1541 NewField(fieldUserID, *cc.ID),
1542 NewField(fieldUserIconID, *cc.Icon),
1543 NewField(fieldUserFlags, *cc.Flags),
1544 ),
1545 )
1546 }
1547
1548 privChat.ClientConn[cc.uint16ID()] = cc
1549
1550 replyFields := []Field{NewField(fieldChatSubject, []byte(privChat.Subject))}
1551 for _, c := range sortedClients(privChat.ClientConn) {
1552 user := User{
1553 ID: *c.ID,
1554 Icon: *c.Icon,
1555 Flags: *c.Flags,
1556 Name: string(c.UserName),
1557 }
1558
1559 replyFields = append(replyFields, NewField(fieldUsernameWithInfo, user.Payload()))
1560 }
1561
1562 res = append(res, cc.NewReply(t, replyFields...))
1563 return res, err
1564 }
1565
1566 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1567 // Fields used in the request:
1568 // * 114 fieldChatID
1569 // Reply is not expected.
1570 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1571 chatID := t.GetField(fieldChatID).Data
1572 chatInt := binary.BigEndian.Uint32(chatID)
1573
1574 privChat := cc.Server.PrivateChats[chatInt]
1575
1576 delete(privChat.ClientConn, cc.uint16ID())
1577
1578 // Notify members of the private chat that the user has left
1579 for _, c := range sortedClients(privChat.ClientConn) {
1580 res = append(res,
1581 *NewTransaction(
1582 tranNotifyChatDeleteUser,
1583 c.ID,
1584 NewField(fieldChatID, chatID),
1585 NewField(fieldUserID, *cc.ID),
1586 ),
1587 )
1588 }
1589
1590 return res, err
1591 }
1592
1593 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1594 // Fields used in the request:
1595 // * 114 Chat ID
1596 // * 115 Chat subject Chat subject string
1597 // Reply is not expected.
1598 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1599 chatID := t.GetField(fieldChatID).Data
1600 chatInt := binary.BigEndian.Uint32(chatID)
1601
1602 privChat := cc.Server.PrivateChats[chatInt]
1603 privChat.Subject = string(t.GetField(fieldChatSubject).Data)
1604
1605 for _, c := range sortedClients(privChat.ClientConn) {
1606 res = append(res,
1607 *NewTransaction(
1608 tranNotifyChatSubject,
1609 c.ID,
1610 NewField(fieldChatID, chatID),
1611 NewField(fieldChatSubject, t.GetField(fieldChatSubject).Data),
1612 ),
1613 )
1614 }
1615
1616 return res, err
1617 }