]> git.r.bdr.sh - rbdr/mobius/blame - internal/mobius/transaction_handlers.go
Allow for personal ~ folder
[rbdr/mobius] / internal / mobius / transaction_handlers.go
CommitLineData
fd740bc4
JH
1package mobius
2
3import (
4 "bufio"
5 "bytes"
6 "encoding/binary"
7 "fmt"
8 "github.com/jhalter/mobius/hotline"
9 "golang.org/x/text/encoding/charmap"
10 "io"
11 "math/big"
12 "os"
13 "path"
14 "path/filepath"
15 "strings"
16 "time"
17)
18
19// Converts bytes from Mac Roman encoding to UTF-8
20var txtDecoder = charmap.Macintosh.NewDecoder()
21
22// Converts bytes from UTF-8 to Mac Roman encoding
23var txtEncoder = charmap.Macintosh.NewEncoder()
24
25// Assign functions to handle specific Hotline transaction types
26func RegisterHandlers(srv *hotline.Server) {
27 srv.HandleFunc(hotline.TranAgreed, HandleTranAgreed)
28 srv.HandleFunc(hotline.TranChatSend, HandleChatSend)
29 srv.HandleFunc(hotline.TranDelNewsArt, HandleDelNewsArt)
30 srv.HandleFunc(hotline.TranDelNewsItem, HandleDelNewsItem)
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)
7bd6f5ad 38 srv.HandleFunc(hotline.TranGetFileNameList, HandleGetFileNameListWithUserFolders)
fd740bc4
JH
39 srv.HandleFunc(hotline.TranGetMsgs, HandleGetMsgs)
40 srv.HandleFunc(hotline.TranGetNewsArtData, HandleGetNewsArtData)
41 srv.HandleFunc(hotline.TranGetNewsArtNameList, HandleGetNewsArtNameList)
42 srv.HandleFunc(hotline.TranGetNewsCatNameList, HandleGetNewsCatNameList)
43 srv.HandleFunc(hotline.TranGetUser, HandleGetUser)
44 srv.HandleFunc(hotline.TranGetUserNameList, HandleGetUserNameList)
45 srv.HandleFunc(hotline.TranInviteNewChat, HandleInviteNewChat)
46 srv.HandleFunc(hotline.TranInviteToChat, HandleInviteToChat)
47 srv.HandleFunc(hotline.TranJoinChat, HandleJoinChat)
48 srv.HandleFunc(hotline.TranKeepAlive, HandleKeepAlive)
49 srv.HandleFunc(hotline.TranLeaveChat, HandleLeaveChat)
50 srv.HandleFunc(hotline.TranListUsers, HandleListUsers)
51 srv.HandleFunc(hotline.TranMoveFile, HandleMoveFile)
52 srv.HandleFunc(hotline.TranNewFolder, HandleNewFolder)
53 srv.HandleFunc(hotline.TranNewNewsCat, HandleNewNewsCat)
54 srv.HandleFunc(hotline.TranNewNewsFldr, HandleNewNewsFldr)
55 srv.HandleFunc(hotline.TranNewUser, HandleNewUser)
56 srv.HandleFunc(hotline.TranUpdateUser, HandleUpdateUser)
57 srv.HandleFunc(hotline.TranOldPostNews, HandleTranOldPostNews)
58 srv.HandleFunc(hotline.TranPostNewsArt, HandlePostNewsArt)
59 srv.HandleFunc(hotline.TranRejectChatInvite, HandleRejectChatInvite)
60 srv.HandleFunc(hotline.TranSendInstantMsg, HandleSendInstantMsg)
61 srv.HandleFunc(hotline.TranSetChatSubject, HandleSetChatSubject)
62 srv.HandleFunc(hotline.TranMakeFileAlias, HandleMakeAlias)
63 srv.HandleFunc(hotline.TranSetClientUserInfo, HandleSetClientUserInfo)
64 srv.HandleFunc(hotline.TranSetFileInfo, HandleSetFileInfo)
65 srv.HandleFunc(hotline.TranSetUser, HandleSetUser)
7bd6f5ad
RBR
66 srv.HandleFunc(hotline.TranUploadFile, HandleUploadFileWithUserFolders)
67 srv.HandleFunc(hotline.TranUploadFldr, HandleUploadFolderWithUserFolders)
fd740bc4
JH
68 srv.HandleFunc(hotline.TranUserBroadcast, HandleUserBroadcast)
69 srv.HandleFunc(hotline.TranDownloadBanner, HandleDownloadBanner)
70}
71
72func HandleChatSend(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
73 if !cc.Authorize(hotline.AccessSendChat) {
74 return cc.NewErrReply(t, "You are not allowed to participate in chat.")
75 }
76
77 // Truncate long usernames
78 // %13.13s: This means a string that is right-aligned in a field of 13 characters.
79 // If the string is longer than 13 characters, it will be truncated to 13 characters.
80 formattedMsg := fmt.Sprintf("\r%13.13s: %s", cc.UserName, t.GetField(hotline.FieldData).Data)
81
82 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
83 // *** Halcyon does stuff
84 // This is indicated by the presence of the optional field FieldChatOptions set to a value of 1.
85 // Most clients do not send this option for normal chat messages.
86 if t.GetField(hotline.FieldChatOptions).Data != nil && bytes.Equal(t.GetField(hotline.FieldChatOptions).Data, []byte{0, 1}) {
87 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(hotline.FieldData).Data)
88 }
89
90 // Truncate the message to the limit. This does not handle the edge case of a string ending on multibyte character.
91 formattedMsg = formattedMsg[:min(len(formattedMsg), hotline.LimitChatMsg)]
92
93 // The ChatID field is used to identify messages as belonging to a private chat.
94 // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
95 chatID := t.GetField(hotline.FieldChatID).Data
96 if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
97
98 // send the message to all connected clients of the private chat
99 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
100 res = append(res, hotline.NewTransaction(
101 hotline.TranChatMsg,
102 c.ID,
103 hotline.NewField(hotline.FieldChatID, chatID),
104 hotline.NewField(hotline.FieldData, []byte(formattedMsg)),
105 ))
106 }
107 return res
108 }
109
110 //cc.Server.mux.Lock()
111 for _, c := range cc.Server.ClientMgr.List() {
112 if c == nil || cc.Account == nil {
113 continue
114 }
115 // Skip clients that do not have the read chat permission.
116 if c.Authorize(hotline.AccessReadChat) {
117 res = append(res, hotline.NewTransaction(hotline.TranChatMsg, c.ID, hotline.NewField(hotline.FieldData, []byte(formattedMsg))))
118 }
119 }
120 //cc.Server.mux.Unlock()
121
122 return res
123}
124
125// HandleSendInstantMsg sends instant message to the user on the current server.
126// Fields used in the request:
127//
128// 103 User Type
129// 113 Options
130// One of the following values:
131// - User message (myOpt_UserMessage = 1)
132// - Refuse message (myOpt_RefuseMessage = 2)
133// - Refuse chat (myOpt_RefuseChat = 3)
134// - Automatic response (myOpt_AutomaticResponse = 4)"
135// 101 Data Optional
136// 214 Quoting message Optional
137//
138// Fields used in the reply:
139// None
140func HandleSendInstantMsg(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
141 if !cc.Authorize(hotline.AccessSendPrivMsg) {
142 return cc.NewErrReply(t, "You are not allowed to send private messages.")
143 }
144
145 msg := t.GetField(hotline.FieldData)
146 userID := t.GetField(hotline.FieldUserID)
147
148 reply := hotline.NewTransaction(
149 hotline.TranServerMsg,
150 [2]byte(userID.Data),
151 hotline.NewField(hotline.FieldData, msg.Data),
152 hotline.NewField(hotline.FieldUserName, cc.UserName),
153 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
154 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
155 )
156
157 // Later versions of Hotline include the original message in the FieldQuotingMsg field so
158 // the receiving client can display both the received message and what it is in reply to
159 if t.GetField(hotline.FieldQuotingMsg).Data != nil {
160 reply.Fields = append(reply.Fields, hotline.NewField(hotline.FieldQuotingMsg, t.GetField(hotline.FieldQuotingMsg).Data))
161 }
162
163 otherClient := cc.Server.ClientMgr.Get([2]byte(userID.Data))
164 if otherClient == nil {
165 return res
166 }
167
168 // Check if target user has "Refuse private messages" flag
169 if otherClient.Flags.IsSet(hotline.UserFlagRefusePM) {
170 res = append(res,
171 hotline.NewTransaction(
172 hotline.TranServerMsg,
173 cc.ID,
174 hotline.NewField(hotline.FieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
175 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
176 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
177 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
178 ),
179 )
180 } else {
181 res = append(res, reply)
182 }
183
184 // Respond with auto reply if other client has it enabled
185 if len(otherClient.AutoReply) > 0 {
186 res = append(res,
187 hotline.NewTransaction(
188 hotline.TranServerMsg,
189 cc.ID,
190 hotline.NewField(hotline.FieldData, otherClient.AutoReply),
191 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
192 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
193 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
194 ),
195 )
196 }
197
198 return append(res, cc.NewReply(t))
199}
200
201var fileTypeFLDR = [4]byte{0x66, 0x6c, 0x64, 0x72}
202
203func HandleGetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
204 fileName := t.GetField(hotline.FieldFileName).Data
205 filePath := t.GetField(hotline.FieldFilePath).Data
206
dcd23d53 207 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
208 if err != nil {
209 return res
210 }
211
212 fw, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
213 if err != nil {
214 return res
215 }
216
217 encodedName, err := txtEncoder.String(fw.Name)
218 if err != nil {
219 return res
220 }
221
222 fields := []hotline.Field{
223 hotline.NewField(hotline.FieldFileName, []byte(encodedName)),
224 hotline.NewField(hotline.FieldFileTypeString, fw.Ffo.FlatFileInformationFork.FriendlyType()),
225 hotline.NewField(hotline.FieldFileCreatorString, fw.Ffo.FlatFileInformationFork.FriendlyCreator()),
226 hotline.NewField(hotline.FieldFileType, fw.Ffo.FlatFileInformationFork.TypeSignature[:]),
227 hotline.NewField(hotline.FieldFileCreateDate, fw.Ffo.FlatFileInformationFork.CreateDate[:]),
228 hotline.NewField(hotline.FieldFileModifyDate, fw.Ffo.FlatFileInformationFork.ModifyDate[:]),
229 }
230
231 // Include the optional FileComment field if there is a comment.
232 if len(fw.Ffo.FlatFileInformationFork.Comment) != 0 {
233 fields = append(fields, hotline.NewField(hotline.FieldFileComment, fw.Ffo.FlatFileInformationFork.Comment))
234 }
235
236 // Include the FileSize field for files.
237 if fw.Ffo.FlatFileInformationFork.TypeSignature != fileTypeFLDR {
238 fields = append(fields, hotline.NewField(hotline.FieldFileSize, fw.TotalSize()))
239 }
240
241 res = append(res, cc.NewReply(t, fields...))
242 return res
243}
244
245// HandleSetFileInfo updates a file or folder Name and/or comment from the Get Info window
246// Fields used in the request:
247// * 201 File Name
248// * 202 File path Optional
249// * 211 File new Name Optional
250// * 210 File comment Optional
251// Fields used in the reply: None
252func HandleSetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
253 fileName := t.GetField(hotline.FieldFileName).Data
254 filePath := t.GetField(hotline.FieldFilePath).Data
255
dcd23d53 256 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
257 if err != nil {
258 return res
259 }
260
261 fi, err := cc.Server.FS.Stat(fullFilePath)
262 if err != nil {
263 return res
264 }
265
266 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
267 if err != nil {
268 return res
269 }
270 if t.GetField(hotline.FieldFileComment).Data != nil {
271 switch mode := fi.Mode(); {
272 case mode.IsDir():
273 if !cc.Authorize(hotline.AccessSetFolderComment) {
274 return cc.NewErrReply(t, "You are not allowed to set comments for folders.")
275 }
276 case mode.IsRegular():
277 if !cc.Authorize(hotline.AccessSetFileComment) {
278 return cc.NewErrReply(t, "You are not allowed to set comments for files.")
279 }
280 }
281
282 if err := hlFile.Ffo.FlatFileInformationFork.SetComment(t.GetField(hotline.FieldFileComment).Data); err != nil {
283 return res
284 }
285 w, err := hlFile.InfoForkWriter()
286 if err != nil {
287 return res
288 }
289 _, err = io.Copy(w, &hlFile.Ffo.FlatFileInformationFork)
290 if err != nil {
291 return res
292 }
293 }
294
dcd23d53 295 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, t.GetField(hotline.FieldFileNewName).Data)
fd740bc4
JH
296 if err != nil {
297 return nil
298 }
299
300 fileNewName := t.GetField(hotline.FieldFileNewName).Data
301
302 if fileNewName != nil {
303 switch mode := fi.Mode(); {
304 case mode.IsDir():
305 if !cc.Authorize(hotline.AccessRenameFolder) {
306 return cc.NewErrReply(t, "You are not allowed to rename folders.")
307 }
308 err = os.Rename(fullFilePath, fullNewFilePath)
309 if os.IsNotExist(err) {
310 return cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found.")
311
312 }
313 case mode.IsRegular():
314 if !cc.Authorize(hotline.AccessRenameFile) {
315 return cc.NewErrReply(t, "You are not allowed to rename files.")
316 }
dcd23d53 317 fileDir, err := hotline.ReadPath(cc.FileRoot(), filePath, []byte{})
fd740bc4
JH
318 if err != nil {
319 return nil
320 }
321 hlFile.Name, err = txtDecoder.String(string(fileNewName))
322 if err != nil {
323 return res
324 }
325
326 err = hlFile.Move(fileDir)
327 if os.IsNotExist(err) {
328 return cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found.")
329 }
330 if err != nil {
331 return res
332 }
333 }
334 }
335
336 res = append(res, cc.NewReply(t))
337 return res
338}
339
340// HandleDeleteFile deletes a file or folder
341// Fields used in the request:
342// * 201 File Name
343// * 202 File path
344// Fields used in the reply: none
345func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
346 fileName := t.GetField(hotline.FieldFileName).Data
347 filePath := t.GetField(hotline.FieldFilePath).Data
348
dcd23d53 349 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
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
384func HandleMoveFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
385 fileName := string(t.GetField(hotline.FieldFileName).Data)
386
dcd23d53 387 filePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
fd740bc4
JH
388 if err != nil {
389 return res
390 }
391
dcd23d53 392 fileNewPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFileNewPath).Data, nil)
fd740bc4
JH
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 }
0ee4d86e 421 // TODO: handle other possible errors; e.g. file delete fails due to permission issue
fd740bc4
JH
422
423 res = append(res, cc.NewReply(t))
424 return res
425}
426
427func 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 }
dcd23d53 449 newFolderPath := path.Join(cc.FileRoot(), subPath, folderName)
fd740bc4
JH
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
470func 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
529func 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
547func 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.
575func 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
721func 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
755func 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
787func 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
809func 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
827func 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
846func 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
900func 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
933func 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 {
9f89cd9f 984 cc.Logger.Error("Error saving ban", "err", err)
fd740bc4
JH
985 }
986 }
987 }
988
fd740bc4
JH
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)
1000func 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 {
9f89cd9f
JH
1007 cc.Logger.Error("get news path", "err", err)
1008 return nil
fd740bc4
JH
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 {
9f89cd9f 1015 cc.Logger.Error("get news categories", "err", err)
fd740bc4
JH
1016 }
1017
1018 fields = append(fields, hotline.NewField(hotline.FieldNewsCatListData15, b))
1019 }
1020
1021 return append(res, cc.NewReply(t, fields...))
1022}
1023
1024func 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
1046func 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
1072func 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”)
1110func 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
1149func HandleDelNewsItem(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1150 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
318eb6bc
JH
1151 if err != nil || len(pathStrs) == 0 {
1152 cc.Logger.Error("invalid news path")
1153 return nil
fd740bc4
JH
1154 }
1155
1156 item := cc.Server.ThreadedNewsMgr.NewsItem(pathStrs)
1157
1158 if item.Type == [2]byte{0, 3} {
1159 if !cc.Authorize(hotline.AccessNewsDeleteCat) {
1160 return cc.NewErrReply(t, "You are not allowed to delete news categories.")
1161 }
1162 } else {
1163 if !cc.Authorize(hotline.AccessNewsDeleteFldr) {
1164 return cc.NewErrReply(t, "You are not allowed to delete news folders.")
1165 }
1166 }
1167
1168 err = cc.Server.ThreadedNewsMgr.DeleteNewsItem(pathStrs)
1169 if err != nil {
1170 return res
1171 }
1172
1173 return append(res, cc.NewReply(t))
1174}
1175
1176// HandleDelNewsArt deletes a threaded news article.
1177// Request Fields
1178// 325 News path
1179// 326 News article Type
1180// 337 News article recursive delete - Delete child articles (1) or not (0)
1181func HandleDelNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1182 if !cc.Authorize(hotline.AccessNewsDeleteArt) {
1183 return cc.NewErrReply(t, "You are not allowed to delete news articles.")
1184
1185 }
1186
1187 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1188 if err != nil {
1189 return res
1190 }
1191
1192 articleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1193 if err != nil {
1194 cc.Logger.Error("error reading article Type", "err", err)
1195 return
1196 }
1197
1198 deleteRecursive := bytes.Equal([]byte{0, 1}, t.GetField(hotline.FieldNewsArtRecurseDel).Data)
1199
1200 err = cc.Server.ThreadedNewsMgr.DeleteArticle(pathStrs, uint32(articleID), deleteRecursive)
1201 if err != nil {
1202 cc.Logger.Error("error deleting news article", "err", err)
1203 }
1204
1205 return []hotline.Transaction{cc.NewReply(t)}
1206}
1207
1208// Request fields
1209// 325 News path
1210// 326 News article Type Type of the parent article?
1211// 328 News article title
1212// 334 News article flags
1213// 327 News article data flavor Currently “text/plain”
1214// 333 News article data
1215func HandlePostNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1216 if !cc.Authorize(hotline.AccessNewsPostArt) {
1217 return cc.NewErrReply(t, "You are not allowed to post news articles.")
1218 }
1219
1220 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
318eb6bc
JH
1221 if err != nil || len(pathStrs) == 0 {
1222 cc.Logger.Error("invalid news path")
fd740bc4
JH
1223 return res
1224 }
1225
1226 parentArticleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1227 if err != nil {
1228 return res
1229 }
1230
1231 err = cc.Server.ThreadedNewsMgr.PostArticle(
1232 pathStrs,
1233 uint32(parentArticleID),
1234 hotline.NewsArtData{
1235 Title: string(t.GetField(hotline.FieldNewsArtTitle).Data),
1236 Poster: string(cc.UserName),
1237 Date: hotline.NewTime(time.Now()),
1238 DataFlav: hotline.NewsFlavor,
1239 Data: string(t.GetField(hotline.FieldNewsArtData).Data),
1240 },
1241 )
1242 if err != nil {
1243 cc.Logger.Error("error posting news article", "err", err)
1244 }
1245
1246 return append(res, cc.NewReply(t))
1247}
1248
1249// HandleGetMsgs returns the flat news data
1250func HandleGetMsgs(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1251 if !cc.Authorize(hotline.AccessNewsReadArt) {
1252 return cc.NewErrReply(t, "You are not allowed to read news.")
1253 }
1254
1255 _, _ = cc.Server.MessageBoard.Seek(0, 0)
1256
1257 newsData, err := io.ReadAll(cc.Server.MessageBoard)
1258 if err != nil {
9f89cd9f 1259 cc.Logger.Error("Error reading messageboard", "err", err)
fd740bc4
JH
1260 }
1261
1262 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldData, newsData)))
1263}
1264
1265func HandleDownloadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1266 if !cc.Authorize(hotline.AccessDownloadFile) {
1267 return cc.NewErrReply(t, "You are not allowed to download files.")
1268 }
1269
1270 fileName := t.GetField(hotline.FieldFileName).Data
1271 filePath := t.GetField(hotline.FieldFilePath).Data
1272 resumeData := t.GetField(hotline.FieldFileResumeData).Data
1273
1274 var dataOffset int64
1275 var frd hotline.FileResumeData
1276 if resumeData != nil {
1277 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1278 return res
1279 }
1280 // TODO: handle rsrc fork offset
1281 dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
1282 }
1283
dcd23d53 1284 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1285 if err != nil {
1286 return res
1287 }
1288
1289 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
1290 if err != nil {
1291 return res
1292 }
1293
1294 xferSize := hlFile.Ffo.TransferSize(0)
1295
dcd23d53
JH
1296 ft := cc.NewFileTransfer(
1297 hotline.FileDownload,
1298 cc.FileRoot(),
1299 fileName,
1300 filePath,
1301 xferSize,
1302 )
fd740bc4 1303
fd740bc4
JH
1304 if resumeData != nil {
1305 var frd hotline.FileResumeData
1306 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1307 return res
1308 }
1309 ft.FileResumeData = &frd
1310 }
1311
1312 // Optional field for when a client requests file preview
1313 // Used only for TEXT, JPEG, GIFF, BMP or PICT files
1314 // The value will always be 2
1315 if t.GetField(hotline.FieldFileTransferOptions).Data != nil {
1316 ft.Options = t.GetField(hotline.FieldFileTransferOptions).Data
1317 xferSize = hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]
1318 }
1319
1320 res = append(res, cc.NewReply(t,
1321 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1322 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1323 hotline.NewField(hotline.FieldTransferSize, xferSize),
1324 hotline.NewField(hotline.FieldFileSize, hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]),
1325 ))
1326
1327 return res
1328}
1329
1330// Download all files from the specified folder and sub-folders
1331func HandleDownloadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
72f8a1fd 1332 if !cc.Authorize(hotline.AccessDownloadFolder) {
fd740bc4
JH
1333 return cc.NewErrReply(t, "You are not allowed to download folders.")
1334 }
1335
dcd23d53 1336 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
fd740bc4 1337 if err != nil {
dcd23d53 1338 return nil
fd740bc4
JH
1339 }
1340
1341 transferSize, err := hotline.CalcTotalSize(fullFilePath)
1342 if err != nil {
dcd23d53 1343 return nil
fd740bc4
JH
1344 }
1345 itemCount, err := hotline.CalcItemCount(fullFilePath)
1346 if err != nil {
dcd23d53 1347 return nil
fd740bc4
JH
1348 }
1349
dcd23d53 1350 fileTransfer := cc.NewFileTransfer(hotline.FolderDownload, cc.FileRoot(), t.GetField(hotline.FieldFileName).Data, t.GetField(hotline.FieldFilePath).Data, transferSize)
fd740bc4
JH
1351
1352 var fp hotline.FilePath
1353 _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data)
1354 if err != nil {
dcd23d53 1355 return nil
fd740bc4
JH
1356 }
1357
1358 res = append(res, cc.NewReply(t,
1359 hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:]),
1360 hotline.NewField(hotline.FieldTransferSize, transferSize),
1361 hotline.NewField(hotline.FieldFolderItemCount, itemCount),
1362 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1363 ))
1364 return res
1365}
1366
1367// Upload all files from the local folder and its subfolders to the specified path on the server
1368// Fields used in the request
1369// 201 File Name
1370// 202 File path
1371// 108 hotline.Transfer size Total size of all items in the folder
1372// 220 Folder item count
1373// 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1374func HandleUploadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
72f8a1fd
JH
1375 if !cc.Authorize(hotline.AccessUploadFolder) {
1376 return cc.NewErrReply(t, "You are not allowed to upload folders.")
1377 }
1378
fd740bc4
JH
1379 var fp hotline.FilePath
1380 if t.GetField(hotline.FieldFilePath).Data != nil {
1381 if _, err := fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1382 return res
1383 }
1384 }
1385
1386 // Handle special cases for Upload and Drop Box folders
1387 if !cc.Authorize(hotline.AccessUploadAnywhere) {
1388 if !fp.IsUploadDir() && !fp.IsDropbox() {
1389 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)))
1390 }
1391 }
1392
1393 fileTransfer := cc.NewFileTransfer(hotline.FolderUpload,
dcd23d53 1394 cc.FileRoot(),
fd740bc4
JH
1395 t.GetField(hotline.FieldFileName).Data,
1396 t.GetField(hotline.FieldFilePath).Data,
1397 t.GetField(hotline.FieldTransferSize).Data,
1398 )
1399
1400 fileTransfer.FolderItemCount = t.GetField(hotline.FieldFolderItemCount).Data
1401
1402 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:])))
1403}
1404
1405// HandleUploadFile
1406// Fields used in the request:
1407// 201 File Name
1408// 202 File path
1409// 204 File transfer options "Optional
1410// Used only to resume download, currently has value 2"
1411// 108 File transfer size "Optional used if download is not resumed"
1412func HandleUploadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1413 if !cc.Authorize(hotline.AccessUploadFile) {
1414 return cc.NewErrReply(t, "You are not allowed to upload files.")
1415 }
1416
1417 fileName := t.GetField(hotline.FieldFileName).Data
1418 filePath := t.GetField(hotline.FieldFilePath).Data
1419 transferOptions := t.GetField(hotline.FieldFileTransferOptions).Data
1420 transferSize := t.GetField(hotline.FieldTransferSize).Data // not sent for resume
1421
1422 var fp hotline.FilePath
1423 if filePath != nil {
1424 if _, err := fp.Write(filePath); err != nil {
1425 return res
1426 }
1427 }
1428
1429 // Handle special cases for Upload and Drop Box folders
1430 if !cc.Authorize(hotline.AccessUploadAnywhere) {
1431 if !fp.IsUploadDir() && !fp.IsDropbox() {
1432 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)))
1433 }
1434 }
dcd23d53 1435 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1436 if err != nil {
1437 return res
1438 }
1439
1440 if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
1441 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different Name.", string(fileName)))
1442 }
1443
dcd23d53 1444 ft := cc.NewFileTransfer(hotline.FileUpload, cc.FileRoot(), fileName, filePath, transferSize)
fd740bc4
JH
1445
1446 replyT := cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]))
1447
1448 // client has requested to resume a partially transferred file
1449 if transferOptions != nil {
1450 fileInfo, err := cc.Server.FS.Stat(fullFilePath + hotline.IncompleteFileSuffix)
1451 if err != nil {
1452 return res
1453 }
1454
1455 offset := make([]byte, 4)
1456 binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
1457
1458 fileResumeData := hotline.NewFileResumeData([]hotline.ForkInfoList{
1459 *hotline.NewForkInfoList(offset),
1460 })
1461
1462 b, _ := fileResumeData.BinaryMarshal()
1463
1464 ft.TransferSize = offset
1465
1466 replyT.Fields = append(replyT.Fields, hotline.NewField(hotline.FieldFileResumeData, b))
1467 }
1468
1469 res = append(res, replyT)
1470 return res
1471}
1472
1473func HandleSetClientUserInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1474 if len(t.GetField(hotline.FieldUserIconID).Data) == 4 {
1475 cc.Icon = t.GetField(hotline.FieldUserIconID).Data[2:]
1476 } else {
1477 cc.Icon = t.GetField(hotline.FieldUserIconID).Data
1478 }
1479 if cc.Authorize(hotline.AccessAnyName) {
1480 cc.UserName = t.GetField(hotline.FieldUserName).Data
1481 }
1482
1483 // the options field is only passed by the client versions > 1.2.3.
1484 options := t.GetField(hotline.FieldOptions).Data
1485 if options != nil {
1486 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1487
1488 cc.Flags.Set(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM))
1489 cc.Flags.Set(hotline.UserFlagRefusePChat, optBitmap.Bit(hotline.UserOptRefuseChat))
1490
1491 // Check auto response
1492 if optBitmap.Bit(hotline.UserOptAutoResponse) == 1 {
1493 cc.AutoReply = t.GetField(hotline.FieldAutomaticResponse).Data
1494 } else {
1495 cc.AutoReply = []byte{}
1496 }
1497 }
1498
1499 for _, c := range cc.Server.ClientMgr.List() {
1500 res = append(res, hotline.NewTransaction(
1501 hotline.TranNotifyChangeUser,
1502 c.ID,
1503 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1504 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1505 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1506 hotline.NewField(hotline.FieldUserName, cc.UserName),
1507 ))
1508 }
1509
1510 return res
1511}
1512
1513// HandleKeepAlive responds to keepalive transactions with an empty reply
1514// * HL 1.9.2 Client sends keepalive msg every 3 minutes
1515// * HL 1.2.3 Client doesn't send keepalives
1516func HandleKeepAlive(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1517 res = append(res, cc.NewReply(t))
1518
1519 return res
1520}
1521
1522func HandleGetFileNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1523 fullPath, err := hotline.ReadPath(
dcd23d53 1524 cc.FileRoot(),
fd740bc4
JH
1525 t.GetField(hotline.FieldFilePath).Data,
1526 nil,
1527 )
1528 if err != nil {
1529 return res
1530 }
1531
1532 var fp hotline.FilePath
1533 if t.GetField(hotline.FieldFilePath).Data != nil {
1534 if _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1535 return res
1536 }
1537 }
1538
1539 // Handle special case for drop box folders
1540 if fp.IsDropbox() && !cc.Authorize(hotline.AccessViewDropBoxes) {
1541 return cc.NewErrReply(t, "You are not allowed to view drop boxes.")
1542 }
1543
1544 fileNames, err := hotline.GetFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
1545 if err != nil {
1546 return res
1547 }
1548
1549 res = append(res, cc.NewReply(t, fileNames...))
1550
1551 return res
1552}
1553
1554// =================================
1555// Hotline private chat flow
1556// =================================
1557// 1. ClientA sends TranInviteNewChat to server with user Type to invite
1558// 2. Server creates new ChatID
1559// 3. Server sends TranInviteToChat to invitee
1560// 4. Server replies to ClientA with new Chat Type
1561//
1562// A dialog box pops up in the invitee client with options to accept or decline the invitation.
1563// If Accepted is clicked:
1564// 1. ClientB sends TranJoinChat with FieldChatID
1565
1566// HandleInviteNewChat invites users to new private chat
1567func HandleInviteNewChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1568 if !cc.Authorize(hotline.AccessOpenChat) {
1569 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1570 }
1571
1572 // Client to Invite
1573 targetID := t.GetField(hotline.FieldUserID).Data
1574
1575 // Create a new chat with self as initial member.
1576 newChatID := cc.Server.ChatMgr.New(cc)
1577
1578 // Check if target user has "Refuse private chat" flag
1579 targetClient := cc.Server.ClientMgr.Get([2]byte(targetID))
1580 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags[:])))
1581 if flagBitmap.Bit(hotline.UserFlagRefusePChat) == 1 {
1582 res = append(res,
1583 hotline.NewTransaction(
1584 hotline.TranServerMsg,
1585 cc.ID,
1586 hotline.NewField(hotline.FieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
1587 hotline.NewField(hotline.FieldUserName, targetClient.UserName),
1588 hotline.NewField(hotline.FieldUserID, targetClient.ID[:]),
1589 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
1590 ),
1591 )
1592 } else {
1593 res = append(res,
1594 hotline.NewTransaction(
1595 hotline.TranInviteToChat,
1596 [2]byte(targetID),
1597 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1598 hotline.NewField(hotline.FieldUserName, cc.UserName),
1599 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1600 ),
1601 )
1602 }
1603
1604 return append(
1605 res,
1606 cc.NewReply(t,
1607 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1608 hotline.NewField(hotline.FieldUserName, cc.UserName),
1609 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1610 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1611 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1612 ),
1613 )
1614}
1615
1616func HandleInviteToChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1617 if !cc.Authorize(hotline.AccessOpenChat) {
1618 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1619 }
1620
1621 // Client to Invite
1622 targetID := t.GetField(hotline.FieldUserID).Data
1623 chatID := t.GetField(hotline.FieldChatID).Data
1624
1625 return []hotline.Transaction{
1626 hotline.NewTransaction(
1627 hotline.TranInviteToChat,
1628 [2]byte(targetID),
1629 hotline.NewField(hotline.FieldChatID, chatID),
1630 hotline.NewField(hotline.FieldUserName, cc.UserName),
1631 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1632 ),
1633 cc.NewReply(
1634 t,
1635 hotline.NewField(hotline.FieldChatID, chatID),
1636 hotline.NewField(hotline.FieldUserName, cc.UserName),
1637 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1638 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1639 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1640 ),
1641 }
1642}
1643
1644func HandleRejectChatInvite(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1645 chatID := [4]byte(t.GetField(hotline.FieldChatID).Data)
1646
1647 for _, c := range cc.Server.ChatMgr.Members(chatID) {
1648 res = append(res,
1649 hotline.NewTransaction(
1650 hotline.TranChatMsg,
1651 c.ID,
1652 hotline.NewField(hotline.FieldChatID, chatID[:]),
1653 hotline.NewField(hotline.FieldData, append(cc.UserName, []byte(" declined invitation to chat")...)),
1654 ),
1655 )
1656 }
1657
1658 return res
1659}
1660
1661// HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1662// Fields used in the reply:
1663// * 115 Chat subject
1664// * 300 User Name with info (Optional)
1665// * 300 (more user names with info)
1666func HandleJoinChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1667 chatID := t.GetField(hotline.FieldChatID).Data
1668
1669 // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
1670 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1671 res = append(res,
1672 hotline.NewTransaction(
1673 hotline.TranNotifyChatChangeUser,
1674 c.ID,
1675 hotline.NewField(hotline.FieldChatID, chatID),
1676 hotline.NewField(hotline.FieldUserName, cc.UserName),
1677 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1678 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1679 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1680 ),
1681 )
1682 }
1683
1684 cc.Server.ChatMgr.Join(hotline.ChatID(chatID), cc)
1685
1686 subject := cc.Server.ChatMgr.GetSubject(hotline.ChatID(chatID))
1687
1688 replyFields := []hotline.Field{hotline.NewField(hotline.FieldChatSubject, []byte(subject))}
1689 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1690 b, err := io.ReadAll(&hotline.User{
1691 ID: c.ID,
1692 Icon: c.Icon,
1693 Flags: c.Flags[:],
1694 Name: string(c.UserName),
1695 })
1696 if err != nil {
1697 return res
1698 }
1699 replyFields = append(replyFields, hotline.NewField(hotline.FieldUsernameWithInfo, b))
1700 }
1701
1702 return append(res, cc.NewReply(t, replyFields...))
1703}
1704
1705// HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1706// Fields used in the request:
1707// - 114 FieldChatID
1708//
1709// Reply is not expected.
1710func HandleLeaveChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1711 chatID := t.GetField(hotline.FieldChatID).Data
1712
1713 cc.Server.ChatMgr.Leave([4]byte(chatID), cc.ID)
1714
1715 // Notify members of the private chat that the user has left
1716 for _, c := range cc.Server.ChatMgr.Members(hotline.ChatID(chatID)) {
1717 res = append(res,
1718 hotline.NewTransaction(
1719 hotline.TranNotifyChatDeleteUser,
1720 c.ID,
1721 hotline.NewField(hotline.FieldChatID, chatID),
1722 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1723 ),
1724 )
1725 }
1726
1727 return res
1728}
1729
1730// HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1731// Fields used in the request:
1732// * 114 Chat Type
1733// * 115 Chat subject
1734// Reply is not expected.
1735func HandleSetChatSubject(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1736 chatID := t.GetField(hotline.FieldChatID).Data
1737
1738 cc.Server.ChatMgr.SetSubject([4]byte(chatID), string(t.GetField(hotline.FieldChatSubject).Data))
1739
1740 // Notify chat members of new subject.
1741 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1742 res = append(res,
1743 hotline.NewTransaction(
1744 hotline.TranNotifyChatSubject,
1745 c.ID,
1746 hotline.NewField(hotline.FieldChatID, chatID),
1747 hotline.NewField(hotline.FieldChatSubject, t.GetField(hotline.FieldChatSubject).Data),
1748 ),
1749 )
1750 }
1751
1752 return res
1753}
1754
1755// HandleMakeAlias makes a file alias using the specified path.
1756// Fields used in the request:
1757// 201 File Name
1758// 202 File path
1759// 212 File new path Destination path
1760//
1761// Fields used in the reply:
1762// None
1763func HandleMakeAlias(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1764 if !cc.Authorize(hotline.AccessMakeAlias) {
1765 return cc.NewErrReply(t, "You are not allowed to make aliases.")
1766 }
1767 fileName := t.GetField(hotline.FieldFileName).Data
1768 filePath := t.GetField(hotline.FieldFilePath).Data
1769 fileNewPath := t.GetField(hotline.FieldFileNewPath).Data
1770
dcd23d53 1771 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
fd740bc4
JH
1772 if err != nil {
1773 return res
1774 }
1775
dcd23d53 1776 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), fileNewPath, fileName)
fd740bc4
JH
1777 if err != nil {
1778 return res
1779 }
1780
fd740bc4
JH
1781 if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1782 return cc.NewErrReply(t, "Error creating alias")
1783 }
1784
1785 res = append(res, cc.NewReply(t))
1786 return res
1787}
1788
1789// HandleDownloadBanner handles requests for a new banner from the server
1790// Fields used in the request:
1791// None
1792// Fields used in the reply:
1793// 107 FieldRefNum Used later for transfer
1794// 108 FieldTransferSize Size of data to be downloaded
1795func HandleDownloadBanner(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
dcd23d53 1796 ft := cc.NewFileTransfer(hotline.BannerDownload, "", []byte{}, []byte{}, make([]byte, 4))
fd740bc4
JH
1797 binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.Banner)))
1798
1799 return append(res, cc.NewReply(t,
1800 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1801 hotline.NewField(hotline.FieldTransferSize, ft.TransferSize),
1802 ))
1803}