Your IP : 216.73.216.224


Current Path : /home/hotlineuser/mobius/hotline/
Upload File :
Current File : //home/hotlineuser/mobius/hotline/transaction.go

package hotline

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"math/rand"
	"slices"
)

type TranType [2]byte

var (
	TranError                = TranType{0x00, 0x00} // 0
	TranGetMsgs              = TranType{0x00, 0x65} // 101
	TranNewMsg               = TranType{0x00, 0x66} // 102
	TranOldPostNews          = TranType{0x00, 0x67} // 103
	TranServerMsg            = TranType{0x00, 0x68} // 104
	TranChatSend             = TranType{0x00, 0x69} // 105
	TranChatMsg              = TranType{0x00, 0x6A} // 106
	TranLogin                = TranType{0x00, 0x6B} // 107
	TranSendInstantMsg       = TranType{0x00, 0x6C} // 108
	TranShowAgreement        = TranType{0x00, 0x6D} // 109
	TranDisconnectUser       = TranType{0x00, 0x6E} // 110
	TranDisconnectMsg        = TranType{0x00, 0x6F} // 111
	TranInviteNewChat        = TranType{0x00, 0x70} // 112
	TranInviteToChat         = TranType{0x00, 0x71} // 113
	TranRejectChatInvite     = TranType{0x00, 0x72} // 114
	TranJoinChat             = TranType{0x00, 0x73} // 115
	TranLeaveChat            = TranType{0x00, 0x74} // 116
	TranNotifyChatChangeUser = TranType{0x00, 0x75} // 117
	TranNotifyChatDeleteUser = TranType{0x00, 0x76} // 118
	TranNotifyChatSubject    = TranType{0x00, 0x77} // 119
	TranSetChatSubject       = TranType{0x00, 0x78} // 120
	TranAgreed               = TranType{0x00, 0x79} // 121
	TranServerBanner         = TranType{0x00, 0x7A} // 122
	TranGetFileNameList      = TranType{0x00, 0xC8} // 200
	TranDownloadFile         = TranType{0x00, 0xCA} // 202
	TranUploadFile           = TranType{0x00, 0xCB} // 203
	TranNewFolder            = TranType{0x00, 0xCD} // 205
	TranDeleteFile           = TranType{0x00, 0xCC} // 204
	TranGetFileInfo          = TranType{0x00, 0xCE} // 206
	TranSetFileInfo          = TranType{0x00, 0xCF} // 207
	TranMoveFile             = TranType{0x00, 0xD0} // 208
	TranMakeFileAlias        = TranType{0x00, 0xD1} // 209
	TranDownloadFldr         = TranType{0x00, 0xD2} // 210
	TranDownloadInfo         = TranType{0x00, 0xD3} // 211
	TranDownloadBanner       = TranType{0x00, 0xD4} // 212
	TranUploadFldr           = TranType{0x00, 0xD5} // 213
	TranGetUserNameList      = TranType{0x01, 0x2C} // 300
	TranNotifyChangeUser     = TranType{0x01, 0x2D} // 301
	TranNotifyDeleteUser     = TranType{0x01, 0x2E} // 302
	TranGetClientInfoText    = TranType{0x01, 0x2F} // 303
	TranSetClientUserInfo    = TranType{0x01, 0x30} // 304
	TranListUsers            = TranType{0x01, 0x5C} // 348
	TranUpdateUser           = TranType{0x01, 0x5D} // 349
	TranNewUser              = TranType{0x01, 0x5E} // 350
	TranDeleteUser           = TranType{0x01, 0x5F} // 351
	TranGetUser              = TranType{0x01, 0x60} // 352
	TranSetUser              = TranType{0x01, 0x61} // 353
	TranUserAccess           = TranType{0x01, 0x62} // 354
	TranUserBroadcast        = TranType{0x01, 0x63} // 355
	TranGetNewsCatNameList   = TranType{0x01, 0x72} // 370
	TranGetNewsArtNameList   = TranType{0x01, 0x73} // 371
	TranDelNewsItem          = TranType{0x01, 0x7C} // 380
	TranNewNewsFldr          = TranType{0x01, 0x7D} // 381
	TranNewNewsCat           = TranType{0x01, 0x7E} // 382
	TranGetNewsArtData       = TranType{0x01, 0x90} // 400
	TranPostNewsArt          = TranType{0x01, 0x9A} // 410
	TranDelNewsArt           = TranType{0x01, 0x9B} // 411
	TranKeepAlive            = TranType{0x01, 0xF4} // 500
)

type Transaction struct {
	Flags      byte     // Reserved (should be 0)
	IsReply    byte     // Request (0) or reply (1)
	Type       TranType // Requested operation (user defined)
	ID         [4]byte  // Unique transaction ID (must be != 0)
	ErrorCode  [4]byte  // Used in the reply (user defined, 0 = no error)
	TotalSize  [4]byte  // Total data size for the fields in this transaction.
	DataSize   [4]byte  // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
	ParamCount [2]byte  // Number of the parameters for this transaction
	Fields     []Field

	ClientID   ClientID // Internal identifier for target client
	readOffset int      // Internal offset to track read progress
}

var tranTypeNames = map[TranType]string{
	TranServerMsg:          "Server Message",
	TranChatMsg:            "Receive chat",
	TranNotifyChangeUser:   "User change",
	TranError:              "Error",
	TranShowAgreement:      "Show agreement",
	TranUserAccess:         "User access",
	TranNotifyDeleteUser:   "User left",
	TranAgreed:             "Accept agreement",
	TranLogin:              "Log In",
	TranChatSend:           "Send chat",
	TranDelNewsArt:         "Delete news article",
	TranDelNewsItem:        "Delete news item",
	TranDeleteFile:         "Delete file",
	TranDeleteUser:         "Delete user",
	TranDisconnectUser:     "Disconnect user",
	TranDownloadFile:       "Download file",
	TranDownloadFldr:       "Download folder",
	TranGetClientInfoText:  "Get client info",
	TranGetFileInfo:        "Get file info",
	TranGetFileNameList:    "Get file list",
	TranGetMsgs:            "Get messages",
	TranGetNewsArtData:     "Get news article",
	TranGetNewsArtNameList: "Get news article list",
	TranGetNewsCatNameList: "Get news categories",
	TranGetUser:            "Get user",
	TranGetUserNameList:    "Get user list",
	TranInviteNewChat:      "Invite to new chat",
	TranInviteToChat:       "Invite to chat",
	TranJoinChat:           "Join chat",
	TranKeepAlive:          "Keepalive",
	TranLeaveChat:          "Leave chat",
	TranListUsers:          "List user accounts",
	TranMoveFile:           "Move file",
	TranNewFolder:          "Create folder",
	TranNewNewsCat:         "Create news category",
	TranNewNewsFldr:        "Create news bundle",
	TranNewUser:            "Create user account",
	TranUpdateUser:         "Update user account",
	TranOldPostNews:        "Post to message board",
	TranPostNewsArt:        "Create news article",
	TranRejectChatInvite:   "Decline chat invite",
	TranSendInstantMsg:     "Send message",
	TranSetChatSubject:     "Set chat subject",
	TranMakeFileAlias:      "Make file alias",
	TranSetClientUserInfo:  "Set client user info",
	TranSetFileInfo:        "Set file info",
	TranSetUser:            "Set user",
	TranUploadFile:         "Upload file",
	TranUploadFldr:         "Upload folder",
	TranUserBroadcast:      "Send broadcast",
	TranDownloadBanner:     "Download banner",
}

// NewTransaction creates a new Transaction with the specified type, client, and optional fields.
func NewTransaction(t TranType, clientID ClientID, fields ...Field) Transaction {
	transaction := Transaction{
		Type:     t,
		ClientID: clientID,
		Fields:   fields,
	}

	// Give the transaction a random ID.
	binary.BigEndian.PutUint32(transaction.ID[:], rand.Uint32())

	return transaction
}

// Write implements io.Writer interface for Transaction.
// Transactions read from the network are read as complete tokens with a bufio.Scanner, so
// the arg p is guaranteed to have the full byte payload of a complete transaction.
func (t *Transaction) Write(p []byte) (n int, err error) {
	// Make sure we have the minimum number of bytes for a transaction.
	if len(p) < 22 {
		return 0, errors.New("buffer too small")
	}

	// Read the total size field.
	totalSize := binary.BigEndian.Uint32(p[12:16])
	tranLen := int(20 + totalSize)

	paramCount := binary.BigEndian.Uint16(p[20:22])

	t.Flags = p[0]
	t.IsReply = p[1]
	copy(t.Type[:], p[2:4])
	copy(t.ID[:], p[4:8])
	copy(t.ErrorCode[:], p[8:12])
	copy(t.TotalSize[:], p[12:16])
	copy(t.DataSize[:], p[16:20])
	copy(t.ParamCount[:], p[20:22])

	scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
	scanner.Split(FieldScanner)

	for i := 0; i < int(paramCount); i++ {
		if !scanner.Scan() {
			return 0, fmt.Errorf("error scanning field: %w", scanner.Err())
		}

		var field Field
		if _, err := field.Write(scanner.Bytes()); err != nil {
			return 0, fmt.Errorf("error reading field: %w", err)
		}
		t.Fields = append(t.Fields, field)
	}

	if err := scanner.Err(); err != nil {
		return 0, fmt.Errorf("scanner error: %w", err)
	}

	return len(p), nil
}

const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields

// transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
	// The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
	if len(data) < 16 {
		return 0, nil, nil
	}

	totalSize := binary.BigEndian.Uint32(data[12:16])

	// tranLen represents the length of bytes that are part of the transaction
	tranLen := int(tranHeaderLen + totalSize)
	if tranLen > len(data) {
		return 0, nil, nil
	}

	return tranLen, data[0:tranLen], nil
}

const minFieldLen = 4

// Read implements the io.Reader interface for Transaction
func (t *Transaction) Read(p []byte) (int, error) {
	payloadSize := t.Size()

	fieldCount := make([]byte, 2)
	binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))

	bbuf := new(bytes.Buffer)

	for _, field := range t.Fields {
		f := field
		_, err := bbuf.ReadFrom(&f)
		if err != nil {
			return 0, fmt.Errorf("error reading field: %w", err)
		}
	}

	buf := slices.Concat(
		[]byte{t.Flags, t.IsReply},
		t.Type[:],
		t.ID[:],
		t.ErrorCode[:],
		payloadSize,
		payloadSize, // this is the dataSize field, but seeming the same as totalSize
		fieldCount,
		bbuf.Bytes(),
	)

	return readFrom(p, &t.readOffset, buf)
}

// Size returns the total size of the transaction payload
func (t *Transaction) Size() []byte {
	bs := make([]byte, 4)

	fieldSize := 0
	for _, field := range t.Fields {
		fieldSize += len(field.Data) + 4
	}

	binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))

	return bs
}

func (t *Transaction) GetField(id FieldType) *Field {
	for _, field := range t.Fields {
		if id == field.Type {
			return &field
		}
	}

	return &Field{}
}