]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/transaction_handlers.go
Merge pull request #88 from jhalter/fix_news_compatability_issues
[rbdr/mobius] / hotline / transaction_handlers.go
index 4866beec065b14e13af3f74784aa25ed203a48df..12b1d4151a49fc1914162730ab5b8ed7e7e0a2a1 100644 (file)
@@ -6,7 +6,6 @@ import (
        "errors"
        "fmt"
        "gopkg.in/yaml.v3"
-       "io/ioutil"
        "math/big"
        "os"
        "path"
@@ -249,14 +248,16 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
        // By holding the option key, Hotline chat allows users to send /me formatted messages like:
        // *** Halcyon does stuff
-       // This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
-       if t.GetField(fieldChatOptions).Data != nil {
+       // This is indicated by the presence of the optional field fieldChatOptions set to a value of 1.
+       // Most clients do not send this option for normal chat messages.
+       if t.GetField(fieldChatOptions).Data != nil && bytes.Equal(t.GetField(fieldChatOptions).Data, []byte{0, 1}) {
                formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
        }
 
+       // The ChatID field is used to identify messages as belonging to a private chat.
+       // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
        chatID := t.GetField(fieldChatID).Data
-       // a non-nil chatID indicates the message belongs to a private chat
-       if chatID != nil {
+       if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
                chatInt := binary.BigEndian.Uint32(chatID)
                privChat := cc.Server.PrivateChats[chatInt]
 
@@ -286,6 +287,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
 // HandleSendInstantMsg sends instant message to the user on the current server.
 // Fields used in the request:
+//
 //     103     User ID
 //     113     Options
 //             One of the following values:
@@ -322,14 +324,29 @@ func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, er
                reply.Fields = append(reply.Fields, NewField(fieldQuotingMsg, t.GetField(fieldQuotingMsg).Data))
        }
 
-       res = append(res, *reply)
-
        id, _ := byteToInt(ID.Data)
        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 {
+               res = append(res,
+                       *NewTransaction(
+                               tranServerMsg,
+                               cc.ID,
+                               NewField(fieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
+                               NewField(fieldUserName, otherClient.UserName),
+                               NewField(fieldUserID, *otherClient.ID),
+                               NewField(fieldOptions, []byte{0, 2}),
+                       ),
+               )
+       } else {
+               res = append(res, *reply)
+       }
+
        // Respond with auto reply if other client has it enabled
        if len(otherClient.AutoReply) > 0 {
                res = append(res,
@@ -463,7 +480,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                                return res, err
                        }
                        if err != nil {
-                               panic(err)
+                               return res, err
                        }
                }
        }
@@ -791,6 +808,15 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        newAccess := accessBitmap{}
                        copy(newAccess[:], getField(fieldUserAccess, &subFields).Data[:])
 
+                       // Prevent account from creating new account with greater permission
+                       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
+                                       }
+                               }
+                       }
+
                        err := cc.Server.NewUser(login, string(getField(fieldUserName, &subFields).Data), string(getField(fieldUserPassword, &subFields).Data), newAccess)
                        if err != nil {
                                return []Transaction{}, err
@@ -820,6 +846,16 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
        newAccess := accessBitmap{}
        copy(newAccess[:], t.GetField(fieldUserAccess).Data[:])
 
+       // Prevent account from creating new account with greater permission
+       for i := 0; i < 64; i++ {
+               if newAccess.IsSet(i) {
+                       if !cc.Authorize(i) {
+                               res = append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself."))
+                               return res, err
+                       }
+               }
+       }
+
        if err := cc.Server.NewUser(login, string(t.GetField(fieldUserName).Data), string(t.GetField(fieldUserPassword).Data), newAccess); err != nil {
                return []Transaction{}, err
        }
@@ -862,17 +898,6 @@ func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err
        return res, err
 }
 
-func byteToInt(bytes []byte) (int, error) {
-       switch len(bytes) {
-       case 2:
-               return int(binary.BigEndian.Uint16(bytes)), nil
-       case 4:
-               return int(binary.BigEndian.Uint32(bytes)), nil
-       }
-
-       return 0, errors.New("unknown byte length")
-}
-
 // HandleGetClientInfoText returns user information for the specific user.
 //
 // Fields used in the request:
@@ -1005,7 +1030,7 @@ func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, e
        cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
 
        // update news on disk
-       if err := ioutil.WriteFile(cc.Server.ConfigDir+"MessageBoard.txt", cc.Server.FlatNews, 0644); err != nil {
+       if err := cc.Server.FS.WriteFile(filepath.Join(cc.Server.ConfigDir, "MessageBoard.txt"), cc.Server.FlatNews, 0644); err != nil {
                return res, err
        }
 
@@ -1163,10 +1188,12 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e
        return res, err
 }
 
+// HandleGetNewsArtData gets the list of article names at the specified news path.
+
 // Fields used in the request:
 // 325 News path       Optional
-//
-// Reply fields:
+
+// Fields used in the reply:
 // 321 News article list data  Optional
 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessNewsReadArt) {
@@ -1189,47 +1216,51 @@ func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction
        return res, err
 }
 
+// HandleGetNewsArtData requests information about the specific news article.
+// Fields used in the request:
+//
+// Request fields
+// 325 News path
+// 326 News article ID
+// 327 News article data flavor
+//
+// Fields used in the reply:
+// 328 News article title
+// 329 News article poster
+// 330 News article date
+// 331 Previous article ID
+// 332 Next article ID
+// 335 Parent article ID
+// 336 First child article ID
+// 327 News article data flavor        "Should be “text/plain”
+// 333 News article data       Optional (if data flavor is “text/plain”)
 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
                return res, err
        }
 
-       // Request fields
-       // 325  News fp
-       // 326  News article ID
-       // 327  News article data flavor
-
-       pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
-
        var cat NewsCategoryListData15
        cats := cc.Server.ThreadedNews.Categories
 
-       for _, fp := range pathStrs {
+       for _, fp := range ReadNewsPath(t.GetField(fieldNewsPath).Data) {
                cat = cats[fp]
                cats = cats[fp].SubCats
        }
-       newsArtID := t.GetField(fieldNewsArtID).Data
 
-       convertedArtID := binary.BigEndian.Uint16(newsArtID)
+       // The official Hotline clients will send the article ID as 2 bytes if possible, but
+       // some third party clients such as Frogblast and Heildrun will always send 4 bytes
+       convertedID, err := byteToInt(t.GetField(fieldNewsArtID).Data)
+       if err != nil {
+               return res, err
+       }
 
-       art := cat.Articles[uint32(convertedArtID)]
+       art := cat.Articles[uint32(convertedID)]
        if art == nil {
                res = append(res, cc.NewReply(t))
                return res, err
        }
 
-       // Reply fields
-       // 328  News article title
-       // 329  News article poster
-       // 330  News article date
-       // 331  Previous article ID
-       // 332  Next article ID
-       // 335  Parent article ID
-       // 336  First child article ID
-       // 327  News article data flavor        "Should be “text/plain”
-       // 333  News article data       Optional (if data flavor is “text/plain”)
-
        res = append(res, cc.NewReply(t,
                NewField(fieldNewsArtTitle, []byte(art.Title)),
                NewField(fieldNewsArtPoster, []byte(art.Poster)),
@@ -1260,7 +1291,7 @@ func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err e
                }
        }
 
-       if bytes.Compare(cats[delName].Type, []byte{0, 3}) == 0 {
+       if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
                if !cc.Authorize(accessNewsDeleteCat) {
                        return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
                }
@@ -1705,15 +1736,33 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err
        targetID := t.GetField(fieldUserID).Data
        newChatID := cc.Server.NewPrivateChat(cc)
 
-       res = append(res,
-               *NewTransaction(
-                       tranInviteToChat,
-                       &targetID,
-                       NewField(fieldChatID, newChatID),
-                       NewField(fieldUserName, cc.UserName),
-                       NewField(fieldUserID, *cc.ID),
-               ),
-       )
+       // Check if target user has "Refuse private chat" flag
+       binary.BigEndian.Uint16(targetID)
+       targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
+
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
+       if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+               res = append(res,
+                       *NewTransaction(
+                               tranServerMsg,
+                               cc.ID,
+                               NewField(fieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
+                               NewField(fieldUserName, targetClient.UserName),
+                               NewField(fieldUserID, *targetClient.ID),
+                               NewField(fieldOptions, []byte{0, 2}),
+                       ),
+               )
+       } else {
+               res = append(res,
+                       *NewTransaction(
+                               tranInviteToChat,
+                               &targetID,
+                               NewField(fieldChatID, newChatID),
+                               NewField(fieldUserName, cc.UserName),
+                               NewField(fieldUserID, *cc.ID),
+                       ),
+               )
+       }
 
        res = append(res,
                cc.NewReply(t,
@@ -1829,7 +1878,8 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
 // Fields used in the request:
-//     * 114   fieldChatID
+//   - 114     fieldChatID
+//
 // Reply is not expected.
 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        chatID := t.GetField(fieldChatID).Data