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