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