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