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