package hotline
import (
+ "bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"gopkg.in/yaml.v3"
+ "io"
"math/big"
"os"
"path"
func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
if !cc.Authorize(accessSendPrivMsg) {
res = append(res, cc.NewErrReply(t, "You are not allowed to send private messages."))
- return res, err
+ return res, errors.New("user is not allowed to send private messages")
}
msg := t.GetField(FieldData)
reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
}
- id, _ := byteToInt(ID.Data)
+ id, err := byteToInt(ID.Data)
+ if err != nil {
+ return res, errors.New("invalid client ID")
+ }
otherClient, ok := cc.Server.Clients[uint16(id)]
if !ok {
return res, errors.New("invalid client ID")
// Check if target user has "Refuse private messages" flag
flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(otherClient.Flags)))
- if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+ if flagBitmap.Bit(UserFlagRefusePM) == 1 {
res = append(res,
*NewTransaction(
TranServerMsg,
return res, err
}
- res = append(res, cc.NewReply(t,
- NewField(FieldFileName, []byte(fw.name)),
+ encodedName, err := txtEncoder.String(fw.name)
+ if err != nil {
+ return res, fmt.Errorf("invalid filepath encoding: %w", err)
+ }
+
+ fields := []Field{
+ NewField(FieldFileName, []byte(encodedName)),
NewField(FieldFileTypeString, fw.ffo.FlatFileInformationFork.friendlyType()),
NewField(FieldFileCreatorString, fw.ffo.FlatFileInformationFork.friendlyCreator()),
- NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment),
NewField(FieldFileType, fw.ffo.FlatFileInformationFork.TypeSignature),
NewField(FieldFileCreateDate, fw.ffo.FlatFileInformationFork.CreateDate),
NewField(FieldFileModifyDate, fw.ffo.FlatFileInformationFork.ModifyDate),
- NewField(FieldFileSize, fw.totalSize()),
- ))
+ }
+
+ // Include the optional FileComment field if there is a comment.
+ if len(fw.ffo.FlatFileInformationFork.Comment) != 0 {
+ fields = append(fields, NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment))
+ }
+
+ // Include the FileSize field for files.
+ if !bytes.Equal(fw.ffo.FlatFileInformationFork.TypeSignature, []byte{0x66, 0x6c, 0x64, 0x72}) {
+ fields = append(fields, NewField(FieldFileSize, fw.totalSize()))
+ }
+
+ res = append(res, cc.NewReply(t, fields...))
return res, err
}
-// HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
+// HandleSetFileInfo updates a file or folder Name and/or comment from the Get Info window
// Fields used in the request:
-// * 201 File name
+// * 201 File Name
// * 202 File path Optional
-// * 211 File new name Optional
+// * 211 File new Name Optional
// * 210 File comment Optional
// Fields used in the reply: None
func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
if err != nil {
return res, err
}
- _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
+ _, err = io.Copy(w, &hlFile.ffo.FlatFileInformationFork)
if err != nil {
return res, err
}
if err != nil {
return nil, err
}
- hlFile.name = string(fileNewName)
+ hlFile.name, err = txtDecoder.String(string(fileNewName))
+ if err != nil {
+ return res, fmt.Errorf("invalid filepath encoding: %w", err)
+ }
+
err = hlFile.move(fileDir)
if os.IsNotExist(err) {
res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
// HandleDeleteFile deletes a file or folder
// Fields used in the request:
-// * 201 File name
+// * 201 File Name
// * 202 File path
// Fields used in the reply: none
func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
return res, err
}
- cc.logger.Infow("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
+ cc.logger.Info("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
if err != nil {
res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
return res, err
}
- if err != nil {
- return res, err
- }
switch mode := fi.Mode(); {
case mode.IsDir():
if !cc.Authorize(accessMoveFolder) {
}
}
newFolderPath := path.Join(cc.Server.Config.FileRoot, subPath, folderName)
+ newFolderPath, err = txtDecoder.String(newFolderPath)
+ if err != nil {
+ return res, fmt.Errorf("invalid filepath encoding: %w", err)
+ }
- // TODO: check path and folder name lengths
+ // TODO: check path and folder Name lengths
if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
- msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
+ msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that Name.", folderName)
return []Transaction{cc.NewErrReply(t, msg)}, nil
}
- // TODO: check for disallowed characters to maintain compatibility for original client
-
if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
return []Transaction{cc.NewErrReply(t, msg)}, nil
return res, err
}
- login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+ login := string(encodeString(t.GetField(FieldUserLogin).Data))
userName := string(t.GetField(FieldUserName).Data)
newAccessLvl := t.GetField(FieldUserAccess).Data
account := cc.Server.Accounts[login]
+ if account == nil {
+ return append(res, cc.NewErrReply(t, "Account not found.")), nil
+ }
account.Name = userName
copy(account.Access[:], newAccessLvl)
if t.GetField(FieldUserPassword).Data == nil {
account.Password = hashAndSalt([]byte(""))
}
- if len(t.GetField(FieldUserPassword).Data) > 1 {
+
+ if !bytes.Equal([]byte{0}, t.GetField(FieldUserPassword).Data) {
account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
}
flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
if c.Authorize(accessDisconUser) {
- flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
+ flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 1)
} else {
- flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
+ flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 0)
}
binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
res = append(res, cc.NewReply(t,
NewField(FieldUserName, []byte(account.Name)),
- NewField(FieldUserLogin, negateString(t.GetField(FieldUserLogin).Data)),
+ NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
NewField(FieldUserPassword, []byte(account.Password)),
NewField(FieldUserAccess, account.Access[:]),
))
var userFields []Field
for _, acc := range cc.Server.Accounts {
- b := make([]byte, 0, 100)
- n, err := acc.Read(b)
+ b, err := io.ReadAll(acc)
if err != nil {
return res, err
}
- userFields = append(userFields, NewField(FieldData, b[:n]))
+ userFields = append(userFields, NewField(FieldData, b))
}
res = append(res, cc.NewReply(t, userFields...))
// performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
for _, field := range t.Fields {
- subFields, err := ReadFields(field.Data[0:2], field.Data[2:])
- if err != nil {
- return res, err
+
+ var subFields []Field
+
+ // Create a new scanner for parsing incoming bytes into transaction tokens
+ scanner := bufio.NewScanner(bytes.NewReader(field.Data[2:]))
+ scanner.Split(fieldScanner)
+
+ for i := 0; i < int(binary.BigEndian.Uint16(field.Data[0:2])); i++ {
+ scanner.Scan()
+
+ var field Field
+ if _, err := field.Write(scanner.Bytes()); err != nil {
+ return res, fmt.Errorf("error reading field: %w", err)
+ }
+ subFields = append(subFields, field)
}
+ // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
if len(subFields) == 1 {
- login := DecodeUserString(getField(FieldData, &subFields).Data)
- cc.logger.Infow("DeleteUser", "login", login)
-
if !cc.Authorize(accessDeleteUser) {
res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
return res, err
}
+ login := string(encodeString(getField(FieldData, &subFields).Data))
+ cc.logger.Info("DeleteUser", "login", login)
+
if err := cc.Server.DeleteUser(login); err != nil {
return res, err
}
continue
}
- login := DecodeUserString(getField(FieldUserLogin, &subFields).Data)
+ // login of the account to update
+ var accountToUpdate, loginToRename string
+
+ // If FieldData is included, this is a rename operation where FieldData contains the login of the existing
+ // account and FieldUserLogin contains the new login.
+ if getField(FieldData, &subFields) != nil {
+ loginToRename = string(encodeString(getField(FieldData, &subFields).Data))
+ }
+ userLogin := string(encodeString(getField(FieldUserLogin, &subFields).Data))
+ if loginToRename != "" {
+ accountToUpdate = loginToRename
+ } else {
+ accountToUpdate = userLogin
+ }
- // check if the login dataFile; if so, we know we are updating an existing user
- if acc, ok := cc.Server.Accounts[login]; ok {
- cc.logger.Infow("UpdateUser", "login", login)
+ // Check if accountToUpdate has an existing account. If so, we know we are updating an existing user.
+ if acc, ok := cc.Server.Accounts[accountToUpdate]; ok {
+ if loginToRename != "" {
+ cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
+ } else {
+ cc.logger.Info("UpdateUser", "login", accountToUpdate)
+ }
- // account dataFile, so this is an update action
+ // account exists, so this is an update action
if !cc.Authorize(accessModifyUser) {
res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
- return res, err
+ return res, nil
}
+ // This part is a bit tricky. There are three possibilities:
+ // 1) The transaction is intended to update the password.
+ // In this case, FieldUserPassword is sent with the new password.
+ // 2) The transaction is intended to remove the password.
+ // In this case, FieldUserPassword is not sent.
+ // 3) The transaction updates the users access bits, but not the password.
+ // In this case, FieldUserPassword is sent with zero as the only byte.
if getField(FieldUserPassword, &subFields) != nil {
newPass := getField(FieldUserPassword, &subFields).Data
- acc.Password = hashAndSalt(newPass)
+ if !bytes.Equal([]byte{0}, newPass) {
+ acc.Password = hashAndSalt(newPass)
+ }
} else {
acc.Password = hashAndSalt([]byte(""))
}
}
err = cc.Server.UpdateUser(
- DecodeUserString(getField(FieldData, &subFields).Data),
- DecodeUserString(getField(FieldUserLogin, &subFields).Data),
+ string(encodeString(getField(FieldData, &subFields).Data)),
+ string(encodeString(getField(FieldUserLogin, &subFields).Data)),
string(getField(FieldUserName, &subFields).Data),
acc.Password,
acc.Access,
return res, err
}
} else {
- cc.logger.Infow("CreateUser", "login", login)
-
if !cc.Authorize(accessCreateUser) {
res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
- return res, err
+ return res, nil
}
+ cc.logger.Info("CreateUser", "login", userLogin)
+
newAccess := accessBitmap{}
copy(newAccess[:], getField(FieldUserAccess, &subFields).Data)
for i := 0; i < 64; i++ {
if newAccess.IsSet(i) {
if !cc.Authorize(i) {
- return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), err
+ return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), nil
}
}
}
- err := cc.Server.NewUser(login, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
+ err = cc.Server.NewUser(userLogin, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
if err != nil {
- return []Transaction{}, err
+ return append(res, cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")), nil
}
}
}
return res, err
}
- login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+ login := string(encodeString(t.GetField(FieldUserLogin).Data))
// If the account already dataFile, reply with an error
if _, ok := cc.Server.Accounts[login]; ok {
}
if err := cc.Server.NewUser(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess); err != nil {
- return []Transaction{}, err
+ res = append(res, cc.NewErrReply(t, "Cannot create account because there is already an account with that login."))
+ return res, err
}
res = append(res, cc.NewReply(t))
func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
if !cc.Authorize(accessDeleteUser) {
res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
- return res, err
+ return res, nil
}
- // TODO: Handle case where account doesn't exist; e.g. delete race condition
- login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+ login := string(encodeString(t.GetField(FieldUserLogin).Data))
if err := cc.Server.DeleteUser(login); err != nil {
return res, err
// 103 User ID
//
// Fields used in the reply:
-// 102 User name
+// 102 User Name
// 101 Data User info text string
func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
if !cc.Authorize(accessGetClientInfo) {
cc.Icon = t.GetField(FieldUserIconID).Data
- cc.logger = cc.logger.With("name", string(cc.UserName))
- cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
+ cc.logger = cc.logger.With("Name", string(cc.UserName))
+ cc.logger.Info("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
options := t.GetField(FieldOptions).Data
optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
// Check refuse private PM option
- if optBitmap.Bit(refusePM) == 1 {
- flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
+ if optBitmap.Bit(UserOptRefusePM) == 1 {
+ flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, 1)
binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
}
// Check refuse private chat option
- if optBitmap.Bit(refuseChat) == 1 {
- flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
+ if optBitmap.Bit(UserOptRefuseChat) == 1 {
+ flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, 1)
binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
}
// Check auto response
- if optBitmap.Bit(autoResponse) == 1 {
+ if optBitmap.Bit(UserOptAutoResponse) == 1 {
cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
} else {
cc.AutoReply = []byte{}
switch t.GetField(FieldOptions).Data[1] {
case 1:
// send message: "You are temporarily banned on this server"
- cc.logger.Infow("Disconnect & temporarily ban " + string(clientConn.UserName))
+ cc.logger.Info("Disconnect & temporarily ban " + string(clientConn.UserName))
res = append(res, *NewTransaction(
TranServerMsg,
banUntil := time.Now().Add(tempBanDuration)
cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil
- cc.Server.writeBanList()
case 2:
// send message: "You are permanently banned on this server"
- cc.logger.Infow("Disconnect & ban " + string(clientConn.UserName))
+ cc.logger.Info("Disconnect & ban " + string(clientConn.UserName))
res = append(res, *NewTransaction(
TranServerMsg,
))
cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
- cc.Server.writeBanList()
+ }
+
+ err := cc.Server.writeBanList()
+ if err != nil {
+ return res, err
}
}
cats := cc.Server.GetNewsCatByPath(pathStrs)
cats[name] = NewsCategoryListData15{
Name: name,
- Type: []byte{0, 3},
+ Type: [2]byte{0, 3},
Articles: map[uint32]*NewsArtData{},
SubCats: make(map[string]NewsCategoryListData15),
}
}
// Fields used in the request:
-// 322 News category name
+// 322 News category Name
// 325 News path
func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
if !cc.Authorize(accessNewsCreateFldr) {
name := string(t.GetField(FieldFileName).Data)
pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
- cc.logger.Infof("Creating new news folder %s", name)
-
cats := cc.Server.GetNewsCatByPath(pathStrs)
cats[name] = NewsCategoryListData15{
Name: name,
- Type: []byte{0, 2},
+ Type: [2]byte{0, 2},
Articles: map[uint32]*NewsArtData{},
SubCats: make(map[string]NewsCategoryListData15),
}
nald := cat.GetNewsArtListData()
- res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, nald.Payload())))
+ b, err := io.ReadAll(&nald)
+ if err != nil {
+
+ }
+
+ res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, b)))
return res, err
}
res = append(res, cc.NewReply(t,
NewField(FieldNewsArtTitle, []byte(art.Title)),
NewField(FieldNewsArtPoster, []byte(art.Poster)),
- NewField(FieldNewsArtDate, art.Date),
- NewField(FieldNewsArtPrevArt, art.PrevArt),
- NewField(FieldNewsArtNextArt, art.NextArt),
- NewField(FieldNewsArtParentArt, art.ParentArt),
- NewField(FieldNewsArt1stChildArt, art.FirstChildArt),
+ NewField(FieldNewsArtDate, art.Date[:]),
+ NewField(FieldNewsArtPrevArt, art.PrevArt[:]),
+ NewField(FieldNewsArtNextArt, art.NextArt[:]),
+ NewField(FieldNewsArtParentArt, art.ParentArt[:]),
+ NewField(FieldNewsArt1stChildArt, art.FirstChildArt[:]),
NewField(FieldNewsArtDataFlav, []byte("text/plain")),
NewField(FieldNewsArtData, []byte(art.Data)),
))
}
}
- if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
+ if cats[delName].Type == [2]byte{0, 3} {
if !cc.Authorize(accessNewsDeleteCat) {
return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
}
Title: string(t.GetField(FieldNewsArtTitle).Data),
Poster: string(cc.UserName),
Date: toHotlineTime(time.Now()),
- PrevArt: []byte{0, 0, 0, 0},
- NextArt: []byte{0, 0, 0, 0},
- ParentArt: bs,
- FirstChildArt: []byte{0, 0, 0, 0},
+ PrevArt: [4]byte{},
+ NextArt: [4]byte{},
+ ParentArt: [4]byte(bs),
+ FirstChildArt: [4]byte{},
DataFlav: []byte("text/plain"),
Data: string(t.GetField(FieldNewsArtData).Data),
}
prevID := uint32(keys[len(keys)-1])
nextID = prevID + 1
- binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
+ binary.BigEndian.PutUint32(newArt.PrevArt[:], prevID)
// Set next article ID
- binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
+ binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt[:], nextID)
}
// Update parent article with first child reply
if parentID != 0 {
parentArt := cat.Articles[parentID]
- if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
- binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
+ if parentArt.FirstChildArt == [4]byte{0, 0, 0, 0} {
+ binary.BigEndian.PutUint32(parentArt.FirstChildArt[:], nextID)
}
}
// Upload all files from the local folder and its subfolders to the specified path on the server
// Fields used in the request
-// 201 File name
+// 201 File Name
// 202 File path
// 108 transfer size Total size of all items in the folder
// 220 Folder item count
// HandleUploadFile
// Fields used in the request:
-// 201 File name
+// 201 File Name
// 202 File path
// 204 File transfer options "Optional
// Used only to resume download, currently has value 2"
}
if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
- res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different name.", string(fileName))))
+ res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different Name.", string(fileName))))
return res, err
}
optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
- flagBitmap.SetBit(flagBitmap, userFlagRefusePM, optBitmap.Bit(refusePM))
+ flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(UserOptRefusePM))
binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
- flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, optBitmap.Bit(refuseChat))
+ flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(UserOptRefuseChat))
binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
// Check auto response
- if optBitmap.Bit(autoResponse) == 1 {
+ if optBitmap.Bit(UserOptAutoResponse) == 1 {
cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
} else {
cc.AutoReply = []byte{}
targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
- if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+ if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
res = append(res,
*NewTransaction(
TranServerMsg,
// HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
// Fields used in the reply:
// * 115 Chat subject
-// * 300 User name with info (Optional)
+// * 300 User Name with info (Optional)
// * 300 (more user names with info)
func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
chatID := t.GetField(FieldChatID).Data
replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
for _, c := range sortedClients(privChat.ClientConn) {
- user := User{
+
+ b, err := io.ReadAll(&User{
ID: *c.ID,
Icon: c.Icon,
Flags: c.Flags,
Name: string(c.UserName),
+ })
+ if err != nil {
+ return res, nil
}
-
- replyFields = append(replyFields, NewField(FieldUsernameWithInfo, user.Payload()))
+ replyFields = append(replyFields, NewField(FieldUsernameWithInfo, b))
}
res = append(res, cc.NewReply(t, replyFields...))
// HandleMakeAlias makes a file alias using the specified path.
// Fields used in the request:
-// 201 File name
+// 201 File Name
// 202 File path
// 212 File new path Destination path
//
return res, err
}
- cc.logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
+ cc.logger.Debug("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
res = append(res, cc.NewErrReply(t, "Error creating alias"))