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