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