diff --git a/byter/byter.go b/byter/byter.go new file mode 100644 index 0000000..88967da --- /dev/null +++ b/byter/byter.go @@ -0,0 +1,30 @@ +package byter + +import ( + "bytes" + "reflect" +) + +var stringLengthTags = map[string]reflect.Kind{ + "uint8": reflect.Uint8, + "uint16": reflect.Uint16, + "uint32": reflect.Uint32, + "uint64": reflect.Uint64, + "int8": reflect.Uint8, + "byte": reflect.Uint8, + "int16": reflect.Int16, + "int32": reflect.Int32, + "int64": reflect.Int64, +} + +type byter struct { + Buff *bytes.Buffer +} + +type EnumMarker interface { + IsEnum() +} + +func (b *byter) isEnum(field reflect.Value) bool { + return field.CanInterface() && reflect.PtrTo(field.Type()).Implements(reflect.TypeOf((*EnumMarker)(nil)).Elem()) +} \ No newline at end of file diff --git a/byter/read.go b/byter/read.go new file mode 100644 index 0000000..c3e5733 --- /dev/null +++ b/byter/read.go @@ -0,0 +1,124 @@ +package byter + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "log" + "reflect" +) + +func NewReader(data []byte) *byter { + return &byter{ + Buff: bytes.NewBuffer(data), + } +} + +func (b *byter) readInteger(kind reflect.Kind, size int, endian string) uint64 { + buffer := make([]byte, size) + _, err := b.Buff.Read(buffer) + if err != nil { + log.Printf("Error reading integer: %v\n", err) + return 0 + } + + if kind == reflect.Uint8 { + return uint64(buffer[0]) + } + + switch endian { + case "big": + switch kind { + case reflect.Uint16: + return uint64(binary.BigEndian.Uint16(buffer)) + case reflect.Uint32: + return uint64(binary.BigEndian.Uint32(buffer)) + case reflect.Uint64: + return binary.BigEndian.Uint64(buffer) + } + case "little": + switch kind { + case reflect.Uint16: + return uint64(binary.LittleEndian.Uint16(buffer)) + case reflect.Uint32: + return uint64(binary.LittleEndian.Uint32(buffer)) + case reflect.Uint64: + return binary.LittleEndian.Uint64(buffer) + } + } + + log.Printf("Unsupported endianness: %s\n", endian) + return 0 +} + +func (b *byter) ReadToStruct(s interface{}) error { + values := reflect.ValueOf(s) + if values.Kind() == reflect.Ptr && values.Elem().Kind() == reflect.Struct { + values = values.Elem() + } else { + return fmt.Errorf("expected a pointer to a struct") + } + + for i := 0; i < values.NumField(); i++ { + field := values.Field(i) + if !field.CanSet() { + continue + } + switch field.Kind() { + case reflect.Bool: + boolByte := b.Buff.Next(1) + field.SetBool(boolByte[0] != 0) + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + hasVLQTag := values.Type().Field(i).Tag.Get("vlq") + if hasVLQTag != "" { + uintValue, err := b.DecodeVLQ() + if err != nil { + return err + } + field.SetUint(uint64(uintValue)) + } else { + size := int(field.Type().Size()) + endianess := values.Type().Field(i).Tag.Get("endian") + uintValue := b.readInteger(field.Kind(), size, endianess) + field.SetUint(uintValue) + } + case reflect.String: + strLen, _ := binary.ReadUvarint(b.Buff) + data := make([]byte, strLen) + b.Buff.Read(data) + field.SetString(string(data)) + case reflect.Interface: + if field.IsNil() { + continue + } + default: + return fmt.Errorf("unsupported type %s for field %s", field.Type(), values.Type().Field(i).Name) + } + } + + return nil +} + +func (b *byter) DecodeVLQ() (int, error) { + multiplier := 1 + value := 0 + var encodedByte byte + var err error + + for { + encodedByte, err = b.Buff.ReadByte() + if err != nil { + return 0, err + } + value += int(encodedByte&0x7F) * multiplier + if multiplier > 2097152 { + return 0, errors.New("malformed remaining length") + } + multiplier *= 128 + if (encodedByte & 0x80) == 0 { + break + } + } + return value, nil +} \ No newline at end of file diff --git a/byter/write.go b/byter/write.go new file mode 100644 index 0000000..4eebab0 --- /dev/null +++ b/byter/write.go @@ -0,0 +1,137 @@ +package byter + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "reflect" +) + +func NewWriter() *byter { + return &byter{ + Buff: bytes.NewBuffer(make([]byte, 0)), + } +} + +func (b *byter) writeInteger(value uint64, kind reflect.Kind, endian string) error { + if kind == reflect.Uint8 { + return b.Buff.WriteByte(byte(value)) + } + + if endian == "" { + endian = "big" + } + switch endian { + case "big": + switch kind { + case reflect.Uint16: + return binary.Write(b.Buff, binary.BigEndian, uint16(value)) + case reflect.Uint32: + return binary.Write(b.Buff, binary.BigEndian, uint32(value)) + case reflect.Uint64: + return binary.Write(b.Buff, binary.BigEndian, value) + } + case "little": + switch kind { + case reflect.Uint16: + return binary.Write(b.Buff, binary.LittleEndian, uint16(value)) + case reflect.Uint32: + return binary.Write(b.Buff, binary.LittleEndian, uint32(value)) + case reflect.Uint64: + return binary.Write(b.Buff, binary.LittleEndian, value) + } + default: + return fmt.Errorf("received unsupported endianness while trying to write %v: %s", kind, endian) + } + + return nil +} + +func (b *byter) WriteFromStruct(s interface{}) ([]byte, error) { + values := reflect.ValueOf(s) + if values.Kind() == reflect.Ptr && values.Elem().Kind() == reflect.Struct { + values = values.Elem() + } else { + return nil, fmt.Errorf("expected a struct") + } + + for i := 0; i < values.NumField(); i++ { + field := values.Field(i) + switch field.Kind() { + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if b.isEnum(field) { + enumValue := uint8(field.Uint()) // TO-DO: support all int vals + b.Buff.WriteByte(enumValue) + continue + } + hasVLQTag := values.Type().Field(i).Tag.Get("vlq") + if hasVLQTag != "" { + err := b.EncodeVLQ(int(field.Uint())) // Convert to int because our VLQ function takes an int + if err != nil { + return nil, err + } + continue + } else { + endianess := values.Type().Field(i).Tag.Get("endian") + err := b.writeInteger(field.Uint(), field.Kind(), endianess) + if err != nil { + return nil, err + } + } + case reflect.String: + str := field.String() + f := values.Type().Field(i) + lengthType := f.Tag.Get("lengthType") + endianess := f.Tag.Get("endian") + err := b.writeString(str, lengthType, endianess) + if err != nil { + return nil, err + } + /* + case reflect.Struct: + sBytes, err := b.WriteFromStruct(field) + if err != nil { + return nil, err + } + b.Buff.Write(sBytes) + */ + default: + log.Printf("Unsupported type %s for field %s\n", field.Type(), values.Type().Field(i).Name) + } + } + + return b.Buff.Bytes(), nil +} + +func (b *byter) writeString(s string, lengthType string, endianess string) error { + if endianess == "" { + endianess = "big" + } + + b.writeInteger(uint64(len(s)), stringLengthTags[lengthType], endianess) + _, err := b.Buff.Write([]byte(s)) + return err +} + +func (b *byter) EncodeVLQ(value int) error { + var encodedByte byte + for { + encodedByte = byte(value & 0x7F) + value >>= 7 + if value > 0 { + encodedByte |= 0x80 + } + + err := b.Buff.WriteByte(encodedByte) + if err != nil { + return err + } + + if value == 0 { + break + } + } + + return nil +} \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..26c6e6c --- /dev/null +++ b/client.go @@ -0,0 +1,91 @@ +package messagix + +import ( + "encoding/json" + "log" + "net/http" + "net/url" + "os" + + "github.com/0xzer/messagix/types" + "github.com/rs/zerolog" +) + +var USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" + +type EventHandler func(evt interface{}) +type Proxy func(*http.Request) (*url.URL, error) +type Client struct { + http *http.Client + socket *Socket + taskManager *TaskManager + eventHandler EventHandler + configs *Configs + + Logger zerolog.Logger + cookies *types.Cookies + proxy Proxy +} + +// pass an empty zerolog.Logger{} for no logging +func NewClient(cookies *types.Cookies, logger zerolog.Logger, proxy Proxy) *Client { + if cookies == nil { + log.Fatal("cookie struct can not be nil") + } + + cli := &Client{ + http: &http.Client{ + Transport: &http.Transport{ + Proxy: proxy, + }, + }, + cookies: cookies, + proxy: proxy, + Logger: logger, + } + + socket := cli.NewSocketClient() + cli.socket = socket + + cli.configs = &Configs{client: cli} + + moduleLoader := &ModuleParser{} + moduleLoader.load() + + + configSetupErr := cli.configs.SetupConfigs() + if configSetupErr != nil { + log.Fatal(configSetupErr) + } + + taskManager := &TaskManager{client: cli, activeTaskIds: make([]int, 0), currTasks: make([]TaskData, 0)} + cli.taskManager = taskManager + + cli.taskManager.AddNewTask(&GetContactsTask{Limit: 100}) + + log.Println(cli.taskManager.FinalizePayload()) + os.Exit(1) + return cli +} + +func (c *Client) SetEventHandler(handler EventHandler) { + c.eventHandler = handler +} + +// Sets the topics the client should subscribe to +func (c *Client) SetTopics(topics []Topic) { + c.socket.setTopics(topics) +} + +func (c *Client) Connect() error { + return c.socket.Connect() +} + +func (c *Client) SaveSession(path string) error { + jsonBytes, err := json.Marshal(c.cookies) + if err != nil { + return err + } + + return os.WriteFile(path, jsonBytes, os.ModePerm) +} \ No newline at end of file diff --git a/codes.go b/codes.go new file mode 100644 index 0000000..b23570f --- /dev/null +++ b/codes.go @@ -0,0 +1,28 @@ +package messagix + +type ConnectionCode uint8 +const ( + CONNECTION_ACCEPTED ConnectionCode = iota + CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION + CONNECTION_REFUSED_IDENTIFIER_REJECTED + CONNECTION_REFUSED_SERVER_UNAVAILABLE + CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD + CONNECTION_REFUSED_UNAUTHORIZED +) + +var connectionCodesNames = map[ConnectionCode]string{ + CONNECTION_ACCEPTED: "CONNECTION_ACCEPTED", + CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: "CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION", + CONNECTION_REFUSED_IDENTIFIER_REJECTED: "CONNECTION_REFUSED_IDENTIFIER_REJECTED", + CONNECTION_REFUSED_SERVER_UNAVAILABLE: "CONNECTION_REFUSED_SERVER_UNAVAILABLE", + CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD: "CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD", + CONNECTION_REFUSED_UNAUTHORIZED: "CONNECTION_REFUSED_UNAUTHORIZED", +} + +func (c ConnectionCode) ToString() string { + if name, ok := connectionCodesNames[c]; ok { + return name + } + return "UNKNOWN_CONNECTION_CODE" +} +func (c ConnectionCode) IsEnum() {} \ No newline at end of file diff --git a/configs.go b/configs.go new file mode 100644 index 0000000..7f32335 --- /dev/null +++ b/configs.go @@ -0,0 +1,68 @@ +package messagix + +import ( + "log" + "strconv" + "github.com/0xzer/messagix/crypto" + "github.com/0xzer/messagix/methods" + "github.com/0xzer/messagix/modules" + "github.com/0xzer/messagix/types" +) + +type Configs struct { + client *Client + mqttConfig *types.MQTTConfig + siteConfig *types.SiteConfig +} + +func (c *Configs) SetupConfigs() error { + schedulerJS := modules.SchedulerJSDefined + if schedulerJS.SiteData == (modules.SiteData{}) { + log.Fatalf("SetupConfigs was somehow called before modules were initalized") + } + + c.mqttConfig = &types.MQTTConfig{ + ProtocolName: "MQIsdp", + ProtocolLevel: 3, + ClientId: "mqttwsclient", + Broker: schedulerJS.MqttWebConfig.Endpoint, + KeepAliveTimeout: 15, + SessionId: methods.GenerateSessionId(), + AppId: schedulerJS.MqttWebConfig.AppID, + ClientCapabilities: schedulerJS.MqttWebConfig.ClientCapabilities, + Capabilities: schedulerJS.MqttWebConfig.Capabilities, + ChatOn: schedulerJS.MqttWebConfig.ChatVisibility, + SubscribedTopics: schedulerJS.MqttWebConfig.SubscribedTopics, + ConnectionType: "websocket", + HostNameOverride: "", + Cid: schedulerJS.MqttWebDeviceID.ClientID, + } + + bitmap := crypto.NewBitmap().Update(modules.Bitmap).ToCompressedString() + csrBitmap := crypto.NewBitmap().Update(modules.CsrBitmap).ToCompressedString() + + eqmcQuery, err := modules.JSONData.Eqmc.ParseAjaxURLData() + if err != nil { + log.Fatalf("failed to parse AjaxURLData from eqmc json struct: %e", err) + } + + c.siteConfig = &types.SiteConfig{ + Bitmap: bitmap, + CSRBitmap: csrBitmap, + HasteSessionId: schedulerJS.SiteData.Hsi, + WebSessionId: methods.GenerateWebsessionID(), + CometReq: eqmcQuery.CometReq, + LsdToken: schedulerJS.LSD.Token, + SpinT: strconv.Itoa(schedulerJS.SiteData.SpinT), + SpinB: schedulerJS.SiteData.SpinB, + SpinR: strconv.Itoa(schedulerJS.SiteData.SpinR), + FbDtsg: schedulerJS.DTSGInitialData.Token, + Jazoest: eqmcQuery.Jazoest, + Pr: strconv.Itoa(schedulerJS.SiteData.Pr), + HasteSession: schedulerJS.SiteData.HasteSession, + ConnectionClass: schedulerJS.WebConnectionClassServerGuess.ConnectionClass, + VersionId: modules.VersionId, + } + + return nil +} \ No newline at end of file diff --git a/crypto/bitmap.go b/crypto/bitmap.go new file mode 100644 index 0000000..884e8dd --- /dev/null +++ b/crypto/bitmap.go @@ -0,0 +1,95 @@ +package crypto + +import ( + "bytes" + "strconv" + "strings" +) + +const charMap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" +type Bitmap struct { + BMap []int + CompressedStr string +} + +func NewBitmap() *Bitmap { + return &Bitmap{BMap: []int{}, CompressedStr: ""} +} + +func (b *Bitmap) Update(data []int) *Bitmap { + maxIndex := -1 + for _, index := range data { + if index > maxIndex { + maxIndex = index + } + } + + if maxIndex >= len(b.BMap) { + expanded := make([]int, maxIndex+1) + copy(expanded, b.BMap) + b.BMap = expanded + } + + for _, index := range data { + if b.BMap[index] == 0 && b.CompressedStr != "" { + b.CompressedStr = "" + } + b.BMap[index] = 1 + } + + return b +} + +func (b *Bitmap) ToCompressedString() *Bitmap { + if len(b.BMap) == 0 { + return b + } + + if b.CompressedStr != "" { + return b + } + + var buf bytes.Buffer + count := 1 + lastValue := b.BMap[0] + buf.WriteString(strconv.Itoa(lastValue)) + for i := 1; i < len(b.BMap); i++ { + currentValue := b.BMap[i] + if currentValue == lastValue { + count++ + } else { + buf.WriteString(encodeRunLength(count)) + lastValue = currentValue + count = 1 + } + } + + if count > 0 { + buf.WriteString(encodeRunLength(count)) + } + + compressedStr := encodeBase64(buf.String()) + b.CompressedStr = compressedStr + return b +} + +func encodeRunLength(num int) string { + binaryStr := strconv.FormatInt(int64(num), 2) + var buf bytes.Buffer + buf.WriteString(strings.Repeat("0", len(binaryStr)-1)) + buf.WriteString(binaryStr) + return buf.String() +} + +// CUSTOM "BASE64" IMPLEMENTATION, DON'T CHANGE +func encodeBase64(binaryStr string) string { + for len(binaryStr)%6 != 0 { + binaryStr += "0" + } + var result string + for i := 0; i < len(binaryStr); i += 6 { + val, _ := strconv.ParseInt(binaryStr[i:i+6], 2, 64) + result += string(charMap[val]) + } + return result +} \ No newline at end of file diff --git a/debug/logger.go b/debug/logger.go new file mode 100644 index 0000000..c45ce9a --- /dev/null +++ b/debug/logger.go @@ -0,0 +1,43 @@ +package debug + +import ( + "fmt" + "time" + "github.com/mattn/go-colorable" + zerolog "github.com/rs/zerolog" +) + +var colors = map[string]string{ + "text": "\x1b[38;5;6m%s\x1b[0m", + "debug": "\x1b[32mDEBUG\x1b[0m", + "gray": "\x1b[38;5;8m%s\x1b[0m", + "info": "\x1b[38;5;111mINFO\x1b[0m", + "error": "\x1b[38;5;204mERROR\x1b[0m", + "fatal": "\x1b[38;5;52mFATAL\x1b[0m", +} + +var output = zerolog.ConsoleWriter{ + Out: colorable.NewColorableStdout(), + TimeFormat: time.ANSIC, + FormatLevel: func(i interface{}) string { + name := fmt.Sprintf("%s", i) + coloredName := colors[name] + return coloredName + }, + FormatMessage: func(i interface{}) string { + coloredMsg := fmt.Sprintf(colors["text"], i) + return coloredMsg + }, + FormatFieldName: func(i interface{}) string { + name := fmt.Sprintf("%s", i) + return fmt.Sprintf(colors["gray"], name+"=") + }, + FormatFieldValue: func(i interface{}) string { + return fmt.Sprintf("%s", i) + }, + NoColor: false, +} + +func NewLogger() zerolog.Logger { + return zerolog.New(output).With().Timestamp().Logger() +} \ No newline at end of file diff --git a/events.go b/events.go new file mode 100644 index 0000000..28fd6f3 --- /dev/null +++ b/events.go @@ -0,0 +1,44 @@ +package messagix + +import "log" + +func (s *Socket) handleBinaryMessage(data []byte) { + s.client.Logger.Debug().Hex("data", data).Msg("Received BinaryMessage") + if s.client.eventHandler == nil { + return + } + + resp := &Response{} + err := resp.Read(data) + log.Println(resp) + if err != nil { + s.handleErrorEvent(err) + } else { + s.client.eventHandler(resp.ResponseData) + } +} + +func (s *Socket) handleErrorEvent(err error) { + errEvent := &Event_Error{Err: err} + s.client.eventHandler(errEvent) +} + +// Event_Ready represents the CONNACK packet's response. +// The library provides the raw parsed data, so handle connection codes as needed for your application. +type Event_Ready struct { + IsNewSession bool + ConnectionCode ConnectionCode +} + +// Event_Error is emitted whenever the library encounters/receives an error. +// These errors can be for example: failed to send data, failed to read response data and so on. +type Event_Error struct { + Err error +} + +// Event_SocketClosed is emitted whenever the websockets CloseHandler() is called. +// This provides great flexability because the user can then decide whether the client should reconnect or not. +type Event_SocketClosed struct { + Code int + Text string +} \ No newline at end of file diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..273c4f8 --- /dev/null +++ b/flags.go @@ -0,0 +1,30 @@ +package messagix + +type ConnectFlags struct { + Username bool + Password bool + Retain bool + QoS uint8 // 0, 1, 2, or 3 + CleanSession bool +} + +func CreateConnectFlagByte(flags ConnectFlags) uint8 { + var connectFlagsByte uint8 + + if flags.Username { + connectFlagsByte |= 0x80 + } + if flags.Password { + connectFlagsByte |= 0x40 + } + if flags.Retain { + connectFlagsByte |= 0x20 + } + + connectFlagsByte |= (flags.QoS << 3) & 0x18 + if flags.CleanSession { + connectFlagsByte |= 0x02 + } + + return connectFlagsByte +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..97f6401 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/0xzer/messagix + +go 1.20 + +require ( + github.com/gorilla/websocket v1.5.0 + github.com/mattn/go-colorable v0.1.13 + github.com/rs/zerolog v1.30.0 +) + +require ( + github.com/mattn/go-isatty v0.0.16 // indirect + golang.org/x/net v0.14.0 + golang.org/x/sys v0.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..58326a2 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/graphql.go b/graphql.go new file mode 100644 index 0000000..04c7c4b --- /dev/null +++ b/graphql.go @@ -0,0 +1,37 @@ +package messagix + +import "net/http" + +type GraphQLPayload struct { + Av string `json:"av,omitempty"` // not required + User string `json:"__user,omitempty"` // not required + A string `json:"__a,omitempty"` // 1 or 0 wether to include "suggestion_keys" or not in the response - no idea what this is + Req string `json:"__req,omitempty"` // not required + Hs string `json:"__hs,omitempty"` // not required + Dpr string `json:"dpr,omitempty"` // not required + Ccg string `json:"__ccg,omitempty"` // not required + Rev string `json:"__rev,omitempty"` // not required + S string `json:"__s,omitempty"` // not required + Hsi string `json:"__hsi,omitempty"` // not required + Dyn string `json:"__dyn,omitempty"` // not required + Csr string `json:"__csr,omitempty"` // not required + CometReq string `json:"__comet_req,omitempty"` // not required but idk what this is + FbDtsg string `json:"fb_dtsg,omitempty"` + Jazoest string `json:"jazoest,omitempty"` // not required + Lsd string `json:"lsd,omitempty"` // not required + SpinR string `json:"__spin_r,omitempty"` // not required + SpinB string `json:"__spin_b,omitempty"` // not required + SpinT string `json:"__spin_t,omitempty"` // not required + FbAPICallerClass string `json:"fb_api_caller_class,omitempty"` // not required + FbAPIReqFriendlyName string `json:"fb_api_req_friendly_name,omitempty"` // not required + Variables string `json:"variables,omitempty"` + ServerTimestamps string `json:"server_timestamps,omitempty"` // "true" or "false" + DocID string `json:"doc_id,omitempty"` +} + +func (c *Client) getGraphQLHeaders() http.Header { + h := http.Header{} + h.Add("user-agent", USER_AGENT) + + return h +} \ No newline at end of file diff --git a/graphql/docs.go b/graphql/docs.go new file mode 100644 index 0000000..e914c04 --- /dev/null +++ b/graphql/docs.go @@ -0,0 +1,5 @@ +package graphql + +var GraphQLDocs = map[string]string{ + +} \ No newline at end of file diff --git a/graphql/responses.go b/graphql/responses.go new file mode 100644 index 0000000..6101f43 --- /dev/null +++ b/graphql/responses.go @@ -0,0 +1,401 @@ +package graphql + +/* + Most likely missing a bunch of data, so if you find more fields in the payload add them +*/ + +type LSPlatformGraphQLLightspeedRequestQuery struct { + Data struct { + Viewer struct { + LightspeedWebRequest struct { + Dependencies []struct { + Name string `json:"name,omitempty"` + Value struct { + Dr string `json:"__dr,omitempty"` + } `json:"value,omitempty"` + } `json:"dependencies,omitempty"` + Experiments any `json:"experiments,omitempty"` + Payload string `json:"payload,omitempty"` + Target string `json:"target,omitempty"` + } `json:"lightspeed_web_request,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometActorGatewayHandlerQuery struct { + Data struct { + XfbActorGatewayOpenExperience any `json:"xfb_actor_gateway_open_experience,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometAppNavigationProfileSwitcherConfigQuery struct { + Data struct { + Viewer struct { + Actor struct { + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + ProfilePicture struct { + URI string `json:"uri,omitempty"` + } `json:"profile_picture,omitempty"` + ProfileSwitcherEligibleProfiles struct { + Count int `json:"count,omitempty"` + } `json:"profile_switcher_eligible_profiles,omitempty"` + } `json:"actor,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + PrefetchUrisV2 []struct { + Label any `json:"label,omitempty"` + URI string `json:"uri,omitempty"` + } `json:"prefetch_uris_v2,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometAppNavigationTargetedTabBarContentInnerImplQuery struct { + Data struct { + Viewer struct { + Actor struct { + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } `json:"actor,omitempty"` + FeedsTabTooltip struct { + Nodes []any `json:"nodes,omitempty"` + } `json:"feeds_tab_tooltip,omitempty"` + TabBookmarks struct { + Edges []struct { + Node struct { + BookmarkedNode struct { + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + } `json:"bookmarked_node,omitempty"` + ID string `json:"id,omitempty"` + LastUsedTimestamp int `json:"last_used_timestamp,omitempty"` + UnreadCount int `json:"unread_count,omitempty"` + } `json:"node,omitempty"` + } `json:"edges,omitempty"` + } `json:"tab_bookmarks,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometLogoutHandlerQuery struct { + Data struct { + LogoutWhitelist []string `json:"logout_whitelist,omitempty"` + Viewer struct { + AccountUser struct { + HasConfirmedContactpoints bool `json:"has_confirmed_contactpoints,omitempty"` + ID string `json:"id,omitempty"` + PendingContactpoints []any `json:"pending_contactpoints,omitempty"` + } `json:"account_user,omitempty"` + LogoutHash string `json:"logout_hash,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometNotificationsBadgeCountQuery struct { + Data struct { + Viewer struct { + NotificationsUnseenCount int `json:"notifications_unseen_count,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometQuickPromotionInterstitialQuery struct { + Data struct { + Viewer struct { + EligiblePromotions struct { + Nodes []any `json:"nodes,omitempty"` + } `json:"eligible_promotions,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometSearchBootstrapKeywordsDataSourceQuery struct { + Data struct { + Viewer struct { + BootstrapKeywords struct { + Edges []struct { + Node struct { + ItemLoggingID string `json:"item_logging_id,omitempty"` + ItemLoggingInfo string `json:"item_logging_info,omitempty"` + KeywordText string `json:"keyword_text,omitempty"` + StsInfo struct { + DirectNavResult struct { + EntID string `json:"ent_id,omitempty"` + EntityType string `json:"entity_type,omitempty"` + ImgURL string `json:"img_url,omitempty"` + LinkURL string `json:"link_url,omitempty"` + Snippet string `json:"snippet,omitempty"` + Title string `json:"title,omitempty"` + Type string `json:"type,omitempty"` + } `json:"direct_nav_result,omitempty"` + DisambiguationResult any `json:"disambiguation_result,omitempty"` + HighConfidenceResult any `json:"high_confidence_result,omitempty"` + } `json:"sts_info,omitempty"` + SuggestionKeys struct { + DefaultKey string `json:"default_key,omitempty"` + Keys []string `json:"keys,omitempty"` + Tier int `json:"tier,omitempty"` + } `json:"suggestion_keys,omitempty"` + } `json:"node,omitempty"` + } `json:"edges,omitempty"` + } `json:"bootstrap_keywords,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometSearchRecentDataSourceQuery struct { + Data struct { + SearchNullState struct { + Results struct { + Edges []struct { + Node struct { + Typename string `json:"__typename,omitempty"` + RenderingStrategy struct { + Typename string `json:"__typename,omitempty"` + Key string `json:"key,omitempty"` + Result struct { + HeaderText string `json:"header_text,omitempty"` + } `json:"result,omitempty"` + } `json:"rendering_strategy,omitempty"` + } `json:"node,omitempty"` + } `json:"edges,omitempty"` + } `json:"results,omitempty"` + } `json:"search_null_state,omitempty"` + Viewer struct { + RecentSearches struct { + Edges []struct { + Node struct { + IsNode string `json:"__isNode,omitempty"` + IsProfile string `json:"__isProfile,omitempty"` + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + ProfilePicture struct { + URI string `json:"uri,omitempty"` + } `json:"profile_picture,omitempty"` + URL string `json:"url,omitempty"` + } `json:"node,omitempty"` + SearchBadge struct { + Enabled bool `json:"enabled,omitempty"` + Snippet any `json:"snippet,omitempty"` + } `json:"search_badge,omitempty"` + } `json:"edges,omitempty"` + } `json:"recent_searches,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometSettingsBadgeQuery struct { + Data struct { + Viewer struct { + DeviceSwitchableAccountHasNotification bool `json:"device_switchable_account_has_notification,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometSettingsDropdownListQuery struct { + Data struct { + EligibleProfiles struct { + Actor struct { + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + ProfileSwitcherEligibleProfiles struct { + Nodes []any `json:"nodes,omitempty"` + } `json:"profile_switcher_eligible_profiles,omitempty"` + } `json:"actor,omitempty"` + } `json:"eligibleProfiles,omitempty"` + FxIdentityManagement struct { + IdentitiesAndCentralIdentities struct { + ID string `json:"id,omitempty"` + LinkedIdentitiesToPci []struct { + Typename string `json:"__typename,omitempty"` + IdentityType string `json:"identity_type,omitempty"` + SwitcherNotificationCount int `json:"switcher_notification_count,omitempty"` + } `json:"linked_identities_to_pci,omitempty"` + } `json:"identities_and_central_identities,omitempty"` + } `json:"fx_identity_management,omitempty"` + FxcalSettings struct { + AcPhase string `json:"ac_phase,omitempty"` + } `json:"fxcal_settings,omitempty"` + IntlLocaleSelectorQuery struct { + CurrentLocale struct { + LocalizedName string `json:"localized_name,omitempty"` + } `json:"current_locale,omitempty"` + } `json:"intl_locale_selector_query,omitempty"` + LogoutWhitelist []string `json:"logout_whitelist,omitempty"` + Viewer struct { + Actor struct { + Typename string `json:"__typename,omitempty"` + CanUserTypeSeeCompanyIdentitySwitcher bool `json:"can_user_type_see_company_identity_switcher,omitempty"` + FirstProfile struct { + Count int `json:"count,omitempty"` + Nodes []any `json:"nodes,omitempty"` + } `json:"first_profile,omitempty"` + ID string `json:"id,omitempty"` + IsAdditionalProfilePlus bool `json:"is_additional_profile_plus,omitempty"` + IsFailingPagePublishingAuthorization bool `json:"is_failing_page_publishing_authorization,omitempty"` + Name string `json:"name,omitempty"` + PagePublishingAuthorizationAdminNotice any `json:"page_publishing_authorization_admin_notice,omitempty"` + PagePublishingAuthorizationHubActionURL string `json:"page_publishing_authorization_hub_action_url,omitempty"` + ProfileSwitcherEligibleProfiles struct { + Count int `json:"count,omitempty"` + } `json:"profileSwitcherEligibleProfiles,omitempty"` + ProfileCount struct { + Count int `json:"count,omitempty"` + } `json:"profile_count,omitempty"` + ProfilePicture struct { + Height int `json:"height,omitempty"` + Scale int `json:"scale,omitempty"` + URI string `json:"uri,omitempty"` + Width int `json:"width,omitempty"` + } `json:"profile_picture,omitempty"` + ProfileSwitcherUnreadNotifications int `json:"profile_switcher_unread_notifications,omitempty"` + Profiles struct { + Edges []any `json:"edges,omitempty"` + PageInfo struct { + EndCursor any `json:"end_cursor,omitempty"` + HasNextPage bool `json:"has_next_page,omitempty"` + } `json:"page_info,omitempty"` + } `json:"profiles,omitempty"` + SettingsDropdownProfilePicture struct { + URI string `json:"uri,omitempty"` + } `json:"settings_dropdown_profile_picture,omitempty"` + ShouldShowAccountLevelSettings bool `json:"should_show_account_level_settings,omitempty"` + UnseenUpdateCount int `json:"unseen_update_count,omitempty"` + Username string `json:"username,omitempty"` + } `json:"actor,omitempty"` + AdditionalProfileCreationEligibility struct { + SingleOwner struct { + CanCreate bool `json:"can_create,omitempty"` + Explanation any `json:"explanation,omitempty"` + } `json:"single_owner,omitempty"` + } `json:"additional_profile_creation_eligibility,omitempty"` + DeviceSwitchableAccounts []any `json:"device_switchable_accounts,omitempty"` + FirstAccount []any `json:"first_account,omitempty"` + IsEligibleForAccountLevelSettings bool `json:"is_eligible_for_account_level_settings,omitempty"` + LogoutHash string `json:"logout_hash,omitempty"` + PrivacyCenter struct { + ShouldShowPrivacyCenter bool `json:"should_show_privacy_center,omitempty"` + } `json:"privacy_center,omitempty"` + SettingsMenuBusinessSuite struct { + BizwebHomeLink string `json:"bizweb_home_link,omitempty"` + ShouldShowEntrypoint bool `json:"should_show_entrypoint,omitempty"` + } `json:"settings_menu_business_suite,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type CometSettingsDropdownTriggerQuery struct { + Data struct { + CoreAppAdminProfileSwitcherNux any `json:"core_app_admin_profile_switcher_nux,omitempty"` + LogoutWhitelist []string `json:"logout_whitelist,omitempty"` + PageManagementNux any `json:"page_management_nux,omitempty"` + ProfileSwitcherAdminEducationNux any `json:"profile_switcher_admin_education_nux,omitempty"` + ProfileSwitcherNux any `json:"profile_switcher_nux,omitempty"` + Viewer struct { + Actor struct { + Typename string `json:"__typename,omitempty"` + ID string `json:"id,omitempty"` + IsAdditionalProfilePlus bool `json:"is_additional_profile_plus,omitempty"` + Name string `json:"name,omitempty"` + ProfilePicture struct { + URI string `json:"uri,omitempty"` + } `json:"profile_picture,omitempty"` + ProfileSwitcherEligibleProfiles struct { + Count int `json:"count,omitempty"` + Nodes []any `json:"nodes,omitempty"` + } `json:"profile_switcher_eligible_profiles,omitempty"` + ProfileTypeNameForContent string `json:"profile_type_name_for_content,omitempty"` + ShouldShowSoapOnboardingDialog bool `json:"should_show_soap_onboarding_dialog,omitempty"` + UserCategoryWithAdminsOrLimitedAccessRoles string `json:"user_category_with_admins_or_limited_access_roles,omitempty"` + Username string `json:"username,omitempty"` + } `json:"actor,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type MWChatBadgeCountQuery struct { + Data struct { + Viewer struct { + MessageThreads struct { + UnseenCount int `json:"unseen_count,omitempty"` + } `json:"message_threads,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type MWChatVideoAutoplaySettingContextQuery struct { + Data struct { + Viewer struct { + VideoSettings struct { + AutoplaySettingWww string `json:"autoplay_setting_www,omitempty"` + } `json:"video_settings,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type MWLSInboxQuery struct { + Data struct { + Viewer struct { + GroupsTab struct { + SuggestedChats []any `json:"suggested_chats,omitempty"` + } `json:"groups_tab,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} + +type PresenceStatusProviderSubscriptionComponentQuery struct { + Data struct { + Viewer struct { + ChatSidebarContactRankings []any `json:"chat_sidebar_contact_rankings,omitempty"` + } `json:"viewer,omitempty"` + } `json:"data,omitempty"` + Extensions struct { + IsFinal bool `json:"is_final,omitempty"` + } `json:"extensions,omitempty"` +} \ No newline at end of file diff --git a/graphql/table.go b/graphql/table.go new file mode 100644 index 0000000..0538772 --- /dev/null +++ b/graphql/table.go @@ -0,0 +1,34 @@ +package graphql + +type GraphQLTable struct { + CometActorGatewayHandlerQuery []CometActorGatewayHandlerQuery + CometAppNavigationProfileSwitcherConfigQuery []CometAppNavigationProfileSwitcherConfigQuery + CometAppNavigationTargetedTabBarContentInnerImplQuery []CometAppNavigationTargetedTabBarContentInnerImplQuery + CometLogoutHandlerQuery []CometLogoutHandlerQuery + CometNotificationsBadgeCountQuery []CometNotificationsBadgeCountQuery + CometQuickPromotionInterstitialQuery []CometQuickPromotionInterstitialQuery + CometSearchBootstrapKeywordsDataSourceQuery []CometSearchBootstrapKeywordsDataSourceQuery + CometSearchRecentDataSourceQuery []CometSearchRecentDataSourceQuery + CometSettingsBadgeQuery []CometSettingsBadgeQuery + CometSettingsDropdownListQuery []CometSettingsDropdownListQuery + CometSettingsDropdownTriggerQuery []CometSettingsDropdownTriggerQuery + MWChatBadgeCountQuery []MWChatBadgeCountQuery + MWChatVideoAutoplaySettingContextQuery []MWChatVideoAutoplaySettingContextQuery + MWLSInboxQuery []MWLSInboxQuery + PresenceStatusProviderSubscriptionComponentQuery []PresenceStatusProviderSubscriptionComponentQuery +} + +type GraphQLPreloader struct { + ActorID any `json:"actorID,omitempty"` + PreloaderID string `json:"preloaderID,omitempty"` + QueryID string `json:"queryID,omitempty"` + Variables Variables `json:"variables,omitempty"` +} + +type Variables struct { + DeviceID string `json:"deviceId,omitempty"` + IncludeChatVisibility bool `json:"includeChatVisibility,omitempty"` + RequestID int `json:"requestId,omitempty"` + RequestPayload string `json:"requestPayload,omitempty"` + RequestType int `json:"requestType,omitempty"` +} \ No newline at end of file diff --git a/http.go b/http.go new file mode 100644 index 0000000..a5d7e4b --- /dev/null +++ b/http.go @@ -0,0 +1,31 @@ +package messagix + +import ( + "bytes" + "io" + "net/http" + + "github.com/0xzer/messagix/types" +) + +func (c *Client) MakeRequest(url string, method string, headers http.Header, payload []byte, contentType types.ContentType) (*http.Response, []byte, error) { + newRequest, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + return nil, nil, err + } + headers.Add("content-type", string(contentType)) + newRequest.Header = headers + + response, err := c.http.Do(newRequest) + if err != nil { + return nil, nil, err + } + defer response.Body.Close() + + responseBody, err := io.ReadAll(response.Body) + if err != nil { + return nil, nil, err + } + + return response, responseBody, nil +} \ No newline at end of file diff --git a/js_module_parser.go b/js_module_parser.go new file mode 100644 index 0000000..b026de7 --- /dev/null +++ b/js_module_parser.go @@ -0,0 +1,101 @@ +package messagix + +import ( + "encoding/json" + "log" + "os" + "strings" + "github.com/0xzer/messagix/modules" + "golang.org/x/net/html" +) + +type ModuleData struct { + Require [][]interface{} `json:"require,omitempty"` +} + +type ScriptTag struct { + Attributes map[string]string + Content string +} + +type ModuleParser struct {} + +func (m *ModuleParser) load() { + docStr, _ := os.ReadFile("res.html") + doc, err := html.Parse(strings.NewReader(string(docStr))) + if err != nil { + log.Fatalf("failed to parse doc string: %e", err) + } + scriptTags := m.findScriptTags(doc) + for _, tag := range scriptTags { + id := tag.Attributes["id"] + switch id { + case "envjson", "__eqmc": + modules.HandleJSON([]byte(tag.Content), id) + default: + if tag.Content == "" { + continue + } + var data *ModuleData + err := json.Unmarshal([]byte(tag.Content), &data) + if err != nil { + os.WriteFile("test.json", []byte(tag.Content), os.ModePerm) + log.Fatalf("failed to unmarshal content to moduleData: %e", err) + } + + req := data.Require + for _, mod := range req { + m.handleModule(mod) + } + } + } +} + +func (m *ModuleParser) handleModule(data []interface{}) { + modName := data[0].(string) + modData := data[3].([]interface{}) + switch modName { + case "ScheduledServerJS", "ScheduledServerJSWithCSS": + method := data[1].(string) + for _, d := range modData { + switch method { + case "handle": + err := modules.SSJSHandle(d) + if err != nil { + log.Fatalf("failed to handle scheduledserverjs module: %e", err) + } + } + } + case "Bootloader": + method := data[1].(string) + for _, d := range modData { + switch method { + case "handlePayload": + err := modules.HandlePayload(d, &modules.SchedulerJSDefined.BootloaderConfig) + if err != nil { + log.Fatalf("failed to handle Bootloader_handlePayload call: %e", err) + } + //debug.Debug().Any("csrBitmap", modules.CsrBitmap).Msg("handlePayload") + } + } + } +} + +func (m *ModuleParser) findScriptTags(n *html.Node) []ScriptTag { + var tags []ScriptTag + if n.Type == html.ElementNode && n.Data == "script" { + attributes := make(map[string]string) + for _, a := range n.Attr { + attributes[a.Key] = a.Val + } + content := "" + if n.FirstChild != nil { + content = n.FirstChild.Data + } + tags = append(tags, ScriptTag{Attributes: attributes, Content: strings.Replace(content, ",BootloaderConfig", ",", -1)}) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + tags = append(tags, m.findScriptTags(c)...) + } + return tags +} \ No newline at end of file diff --git a/json.go b/json.go new file mode 100644 index 0000000..cbda30a --- /dev/null +++ b/json.go @@ -0,0 +1,55 @@ +package messagix + +import ( + "encoding/json" +) + +type Connect struct { + AccountId string `json:"u"` // account id + SessionId int64 `json:"s"` // randomly generated sessionid + ClientCapabilities int `json:"cp"` // mqttconfig clientCapabilities (3) + Capabilities int `json:"ecp"` // mqttconfig capabilities (10) + ChatOn bool `json:"chat_on"` // mqttconfig chatVisibility (true) - not 100% sure + Fg bool `json:"fg"` + Cid string `json:"d"` // cid from html content + ConnectionType string `json:"ct"` // connection type? websocket + MqttSid string `json:"mqtt_sid"` // "" + AppId int64 `json:"aid"` // mqttconfig appID (219994525426954) + SubscribedTopics []any `json:"st"` // mqttconfig subscribedTopics ([]) + Pm []any `json:"pm"` // only seen empty array + Dc string `json:"dc"` // only seem empty string + NoAutoFg bool `json:"no_auto_fg"` // only seen true + Gas any `json:"gas"` // only seen null + Pack []any `json:"pack"` // only seen empty arr + HostNameOverride string `json:"php_override"` // mqttconfig hostNameOverride ("") - not 100% sure + P any `json:"p"` // only seen null + UserAgent string `json:"a"` // user agent + Aids any `json:"aids"` // only seen null +} + +func (s *Socket) newConnectJSON() (string, error) { + payload := &Connect{ + AccountId: s.client.cookies.AccountId, + SessionId: s.client.configs.mqttConfig.SessionId, + ClientCapabilities: s.client.configs.mqttConfig.ClientCapabilities, + Capabilities: s.client.configs.mqttConfig.Capabilities, + ChatOn: s.client.configs.mqttConfig.ChatOn, + Fg: true, + ConnectionType: s.client.configs.mqttConfig.ConnectionType, + MqttSid: "", + AppId: s.client.configs.mqttConfig.AppId, + SubscribedTopics: s.client.configs.mqttConfig.SubscribedTopics, + Pm: make([]any, 0), + Dc: "", + NoAutoFg: true, + Gas: nil, + Pack: make([]any, 0), + HostNameOverride: s.client.configs.mqttConfig.HostNameOverride, + P: nil, + UserAgent: USER_AGENT, + Aids: nil, + } + + jsonData, err := json.Marshal(payload) + return string(jsonData), err +} \ No newline at end of file diff --git a/lightspeed/decode.go b/lightspeed/decode.go new file mode 100644 index 0000000..a2a952d --- /dev/null +++ b/lightspeed/decode.go @@ -0,0 +1,193 @@ +package lightspeed + +import ( + "fmt" + "log" + "os" + "reflect" + "strconv" + "strings" +) + +type LightSpeedDecoder struct { + Table interface{} // struct that contains pointers to all the dependencies/stores + Dependencies map[string]string + StatementReferences map[int]int64 +} + +func NewLightSpeedDecoder(dependencies map[string]string, table interface{}) *LightSpeedDecoder { + return &LightSpeedDecoder{ + Table: table, + Dependencies: dependencies, + StatementReferences: make(map[int]int64), + } +} + +func (ls *LightSpeedDecoder) Decode(data interface{}) interface{} { + s, ok := data.([]interface{}) + if !ok { + return data + } + + stepType := StepType(int(s[0].(float64))) + stepData := s[1:] + switch stepType { + case BLOCK: + for _, blockData := range stepData { + stepDataArr := blockData.([]interface{}) + ls.Decode(stepDataArr) + } + case LOAD: + key, ok := stepData[0].(float64) + if !ok { + log.Println("[LOAD] failed to store key to float64") + return false + } + + shouldLoad, ok := ls.StatementReferences[int(key)] + if !ok { + log.Println("[LOAD] failed to fetch statement reference for key:", key) + return false + } + return shouldLoad + case STORE: + retVal := ls.Decode(stepData[1]) + ls.StatementReferences[int(stepData[0].(float64))] = retVal.(int64) + //log.Println(ls.StatementReferences) + case STORE_ARRAY: + key, ok := stepData[0].(float64) + if !ok { + log.Println(stepData...) + os.Exit(1) + } + + shouldStore, ok := stepData[1].(float64) + if !ok { + log.Println(stepData...) + os.Exit(1) + } + + ls.StatementReferences[int(key)] = int64(shouldStore) + ls.Decode(s[2:]) + case CALL_STORED_PROCEDURE: + referenceName := stepData[0].(string) + ls.handleStoredProcedure(referenceName, stepData[1:]) + case UNDEFINED: + return nil + case I64_FROM_STRING: + i64, err := strconv.ParseInt(stepData[0].(string), 10, 64) + if err != nil { + log.Println("[I64_FROM_STRING] failed to convert string to int64:", err.Error()) + return 0 + } + return i64 + case IF: + statement := stepData[0] + result := ls.Decode(statement).(int64) + if result > 0 { + ls.Decode(stepData[1]) + } else if len(stepData) >= 3 { + if stepData[2] != nil { + ls.Decode(stepData[2]) + } + } + default: + log.Println("got unknown step type:", stepType) + os.Exit(1) + } + + return nil +} + +func (ls *LightSpeedDecoder) handleStoredProcedure(referenceName string, data []interface{}) { + depReference, ok := ls.Dependencies[referenceName] + if !ok { + log.Println("Skipping dependency with reference name:",referenceName, data) + return + } + + reflectedMs := reflect.ValueOf(ls.Table).Elem() + //log.Println(depReference) + depField := reflectedMs.FieldByName(depReference) + + if !depField.IsValid() { + log.Println("Skipping dependency with reference name:",referenceName, data) + return + } + var err error + // get the Type of the elements of the slice + depFieldsType := depField.Type().Elem() + + // create a new instance of the underlying type + newDepInstance := reflect.New(depFieldsType).Elem() + for i := 0; i < depFieldsType.NumField(); i++ { + fieldInfo := depFieldsType.Field(i) + var index int + conditionField := fieldInfo.Tag.Get("conditionField") + if conditionField != "" { + indexChoices := fieldInfo.Tag.Get("indexes") + conditionVal := newDepInstance.FieldByName(conditionField) + index, err = ls.parseConditionIndex(conditionVal.Bool(), indexChoices) + if err != nil { + log.Println(fmt.Sprintf("failed to parse condition index in dependency %v for field %v", depFieldsType.Name(), fieldInfo.Name)) + continue + } + } else { + index, _ = strconv.Atoi(fieldInfo.Tag.Get("index")) + } + + kind := fieldInfo.Type.Kind() + val := ls.Decode(data[index]) + if val == nil { // skip setting field, because the index in the array was [9] which is undefined. + continue + } + + switch kind { + case reflect.Int64: + i64, ok := val.(int64) + if !ok { + log.Println(fmt.Sprintf("failed to set int64 to %v in dependency %v for field %v", val, depFieldsType.Name(), fieldInfo.Name)) + continue + } + newDepInstance.Field(i).SetInt(i64) + case reflect.String: + str, ok := val.(string) + if !ok { + log.Println(fmt.Sprintf("failed to set string to %v in dependency %v for field %v", val, depFieldsType.Name(), fieldInfo.Name)) + continue + } + newDepInstance.Field(i).SetString(str) + case reflect.Interface: + if val == nil { + continue + } + log.Println(val) + newDepInstance.Field(i).Set(reflect.ValueOf(val)) + case reflect.Bool: + boolean, ok := val.(bool) + if !ok { + log.Println(fmt.Sprintf("failed to set bool to %v in dependency %v for field %v", val, depFieldsType.Name(), fieldInfo.Name)) + continue + } + newDepInstance.Field(i).SetBool(boolean) + default: + log.Println("invalid kind:", kind, val) + os.Exit(1) + } + } + newSlice := reflect.Append(depField, newDepInstance) + depField.Set(newSlice) +} + +// conditionVal ? trueIndex : falseIndex +func (ls *LightSpeedDecoder) parseConditionIndex(val bool, choices string) (int, error) { + indexes := strings.Split(choices, ",") + var index int + var err error + if val { + index, err = strconv.Atoi(indexes[0]) + } else { + index, err = strconv.Atoi(indexes[1]) + } + return index, err +} \ No newline at end of file diff --git a/lightspeed/lightspeed.go b/lightspeed/lightspeed.go new file mode 100644 index 0000000..7eae8cf --- /dev/null +++ b/lightspeed/lightspeed.go @@ -0,0 +1,115 @@ +package lightspeed + +import ( + "encoding/json" + "log" +) + +type StepType int +const ( + BLOCK StepType = 1 + LOAD StepType = 2 + STORE StepType = 3 + STORE_ARRAY StepType = 4 + CALL_STORED_PROCEDURE StepType = 5 + CALL_NATIVE_TYPE_OPERATION StepType = 6 + CALL_NATIVE_OPERATION StepType = 7 + LIST StepType = 8 + UNDEFINED StepType = 9 + INFINITY StepType = 10 + NAN StepType = 11 + RETURN StepType = 12 + BOOL_TO_STR StepType = 13 + BLOBS_TO_STRING StepType = 14 + BLOBS_OF_STRING StepType = 15 + TO_BLOB StepType = 16 + I64_OF_FLOAT StepType = 17 + I64_TO_FLOAT StepType = 18 + I64_FROM_STRING StepType = 19 + I64_TO_STRING StepType = 20 + READ_GK StepType = 21 + READ_QE StepType = 22 + IF StepType = 23 + OR StepType = 24 + AND StepType = 25 + NOT StepType = 26 + IS_NULL StepType = 27 + ENFORCE_NOT_NULL StepType = 28 + GENERIC_EQUAL StepType = 29 + I64_EQUAL StepType = 30 + BLOB_EQUAL StepType = 31 + GENERIC_NOT_EQUAL StepType = 32 + I64_NOT_EQUAL StepType = 33 + BLOB_NOT_EQUAL StepType = 34 + GENERIC_GREATER_THAN StepType = 35 + I64_GREATER_THAN StepType = 36 + BLOB_GREATER_THAN StepType = 37 + GENERIC_GREATER_THAN_OR_EQUAL StepType = 38 + I64_GREATER_THAN_OR_EQUAL StepType = 39 + BLOB_GREATER_THAN_OR_EQUAL StepType = 40 + GENERIC_LESS_THAN StepType = 41 + I64_LESS_THAN StepType = 42 + BLOB_LESS_THAN StepType = 43 + GENERIC_LESS_THAN_OR_EQUAL StepType = 44 + I64_LESS_THAN_OR_EQUAL StepType = 45 + BLOB_LESS_THAN_OR_EQUAL StepType = 46 + THROW StepType = 47 + LOG_CONSOLE StepType = 48 + LOGGER_LOG StepType = 49 + NATIVE_OP_ARRAY_CREATE StepType = 50 + NATIVE_OP_ARRAY_APPEND StepType = 51 + NATIVE_OP_ARRAY_GET_SIZE StepType = 52 + NATIVE_OP_MAP_CREATE StepType = 53 + NATIVE_OP_MAP_GET StepType = 54 + NATIVE_OP_MAP_SET StepType = 55 + NATIVE_OP_MAP_KEYS StepType = 56 + NATIVE_OP_MAP_DELETE StepType = 57 + NATIVE_OP_MAP_HAS StepType = 58 + NATIVE_OP_STR_JOIN StepType = 59 + NATIVE_OP_CURRENT_TIME StepType = 60 + NATIVE_OP_JSON_STRINGIFY StepType = 61 + NATIVE_OP_RNG_NUM StepType = 62 + NATIVE_OP_LOCALIZATION_SUPPORTED StepType = 63 + NATIVE_OP_LOCALIZATION_SUPPORTED_V2 StepType = 64 + NATIVE_OP_RESOLVE_LOCALIZED StepType = 65 + NATIVE_OP_RESOLVE_LOCALIZED_V2 StepType = 66 + ADD StepType = 68 + I64_ADD StepType = 69 + I64_CAST StepType = 70 + READ_JUSTKNOB StepType = 71 + READ_IGGK StepType = 72 +) + +type LightSpeedData struct { + Name string `json:"name"` + Steps interface{} `json:"step"` +} + +type Dependency struct { + Name string `json:"name,omitempty"` + Value DependencyValue `json:"value,omitempty"` +} + +type DependencyValue struct { + ReferenceName string `json:"__dr,omitempty"` +} + +func DependenciesToMap(dep interface{}) map[string]string { + var converted []Dependency + b, err := json.Marshal(dep) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(b, &converted) + if err != nil { + log.Fatal(err) + } + + depMap := make(map[string]string, 0) + for _, d := range converted { + depMap[d.Name] = d.Value.ReferenceName + } + + return depMap +} \ No newline at end of file diff --git a/methods/methods.go b/methods/methods.go new file mode 100644 index 0000000..4f2fc53 --- /dev/null +++ b/methods/methods.go @@ -0,0 +1,37 @@ +package methods + +import ( + "math/rand" + "strconv" + "time" +) + +var Charset = []rune("abcdefghijklmnopqrstuvwxyz1234567890") + +func GenerateTimestampString() string { + return strconv.Itoa(int(time.Now().UnixMilli())) +} + +func GenerateSessionId() int64 { + min := int64(2171078810009599) + max := int64(4613554604867583) + return rand.Int63n(max-min+1) + min +} + +func RandStr(length int) string { + b := make([]rune, length) + for i := range b { + b[i] = Charset[rand.Intn(len(Charset))] + } + return string(b) +} + +func GenerateWebsessionID() string { + return RandStr(6) + ":" + RandStr(6) + ":" + RandStr(6) +} + +func GenerateEpochId() int64 { + timestamp := time.Now().UnixNano() / int64(time.Millisecond) + id := (timestamp << 22) | (42 << 12) + return id +} \ No newline at end of file diff --git a/modules/account.go b/modules/account.go new file mode 100644 index 0000000..8c08cff --- /dev/null +++ b/modules/account.go @@ -0,0 +1,81 @@ +package modules + +type CurrentBusinessAccount struct { + BusinessAccountName any `json:"businessAccountName,omitempty"` + BusinessID any `json:"business_id,omitempty"` + BusinessPersonaID any `json:"business_persona_id,omitempty"` + BusinessProfilePicURL any `json:"business_profile_pic_url,omitempty"` + BusinessRole any `json:"business_role,omitempty"` + BusinessUserID any `json:"business_user_id,omitempty"` + Email any `json:"email,omitempty"` + EnterpriseProfilePicURL any `json:"enterprise_profile_pic_url,omitempty"` + ExpiryTime any `json:"expiry_time,omitempty"` + FirstName any `json:"first_name,omitempty"` + HasVerifiedEmail any `json:"has_verified_email,omitempty"` + IPPermission any `json:"ip_permission,omitempty"` + IsBusinessPerson bool `json:"isBusinessPerson,omitempty"` + IsEnterpriseBusiness bool `json:"isEnterpriseBusiness,omitempty"` + IsFacebookWorkAccount bool `json:"isFacebookWorkAccount,omitempty"` + IsInstagramBusinessPerson bool `json:"isInstagramBusinessPerson,omitempty"` + IsTwoFacNewFlow bool `json:"isTwoFacNewFlow,omitempty"` + IsUserOptInAccountSwitchInfraUpgrade bool `json:"isUserOptInAccountSwitchInfraUpgrade,omitempty"` + IsAdsFeatureLimited any `json:"is_ads_feature_limited,omitempty"` + IsBusinessBanhammered any `json:"is_business_banhammered,omitempty"` + LastName any `json:"last_name,omitempty"` + PermittedBusinessAccountTaskIds []any `json:"permitted_business_account_task_ids,omitempty"` + PersonalUserID string `json:"personal_user_id,omitempty"` + ShouldHideComponentsByUnsupportedFirstPartyTools bool `json:"shouldHideComponentsByUnsupportedFirstPartyTools,omitempty"` + ShouldShowAccountSwitchComponents bool `json:"shouldShowAccountSwitchComponents,omitempty"` +} + +type MessengerWebInitData struct { + AccountKey string `json:"accountKey,omitempty"` + AppID int64 `json:"appId,omitempty"` + CryptoAuthToken CryptoAuthToken `json:"cryptoAuthToken,omitempty"` + LogoutToken string `json:"logoutToken,omitempty"` + SessionID string `json:"sessionId,omitempty"` +} + +type CryptoAuthToken struct { + EncryptedSerializedCat string `json:"encrypted_serialized_cat,omitempty"` + ExpirationTimeInSeconds int `json:"expiration_time_in_seconds,omitempty"` +} + +type LSD struct { + Token string `json:"token,omitempty"` +} + +type IntlViewerContext struct { + Gender int `json:"GENDER,omitempty"` + RegionalLocale any `json:"regionalLocale,omitempty"` +} + +type IntlCurrentLocale struct { + Code string `json:"code,omitempty"` +} + +type DTSGInitData struct { + AsyncGetToken string `json:"async_get_token,omitempty"` + Token string `json:"token,omitempty"` +} + +type DTSGInitialData struct { + Token string `json:"token,omitempty"` +} + +type CurrentUserInitialData struct { + AccountID string `json:"ACCOUNT_ID,omitempty"` + AppID string `json:"APP_ID,omitempty"` + HasSecondaryBusinessPerson bool `json:"HAS_SECONDARY_BUSINESS_PERSON,omitempty"` + IsBusinessDomain bool `json:"IS_BUSINESS_DOMAIN,omitempty"` + IsBusinessPersonAccount bool `json:"IS_BUSINESS_PERSON_ACCOUNT,omitempty"` + IsDeactivatedAllowedOnMessenger bool `json:"IS_DEACTIVATED_ALLOWED_ON_MESSENGER,omitempty"` + IsFacebookWorkAccount bool `json:"IS_FACEBOOK_WORK_ACCOUNT,omitempty"` + IsMessengerCallGuestUser bool `json:"IS_MESSENGER_CALL_GUEST_USER,omitempty"` + IsMessengerOnlyUser bool `json:"IS_MESSENGER_ONLY_USER,omitempty"` + IsWorkroomsUser bool `json:"IS_WORKROOMS_USER,omitempty"` + IsWorkMessengerCallGuestUser bool `json:"IS_WORK_MESSENGER_CALL_GUEST_USER,omitempty"` + Name string `json:"NAME,omitempty"` + ShortName string `json:"SHORT_NAME,omitempty"` + UserID string `json:"USER_ID,omitempty"` +} \ No newline at end of file diff --git a/modules/bootloader.go b/modules/bootloader.go new file mode 100644 index 0000000..e0b0e81 --- /dev/null +++ b/modules/bootloader.go @@ -0,0 +1,83 @@ +package modules + +import ( + "log" + "strconv" + "strings" +) + +type BootLoaderConfig struct { + BtCutoffIndex int `json:"btCutoffIndex,omitempty"` + DeferBootloads bool `json:"deferBootloads,omitempty"` + EarlyRequireLazy bool `json:"earlyRequireLazy,omitempty"` + FastPathForAlreadyRequired bool `json:"fastPathForAlreadyRequired,omitempty"` + HypStep4 bool `json:"hypStep4,omitempty"` + JsRetries []int `json:"jsRetries,omitempty"` + JsRetryAbortNum int `json:"jsRetryAbortNum,omitempty"` + JsRetryAbortTime int `json:"jsRetryAbortTime,omitempty"` + PhdOn bool `json:"phdOn,omitempty"` + SilentDups bool `json:"silentDups,omitempty"` + Timeout int `json:"timeout,omitempty"` + TranslationRetries []int `json:"translationRetries,omitempty"` + TranslationRetryAbortNum int `json:"translationRetryAbortNum,omitempty"` + TranslationRetryAbortTime int `json:"translationRetryAbortTime,omitempty"` +} + +type Bootloader_HandlePayload struct { + Consistency Consistency `json:"consistency,omitempty"` + RsrcMap map[string]RsrcDetails `json:"rsrcMap,omitempty"` + CsrUpgrade string `json:"csrUpgrade,omitempty"` +} + +type Consistency struct { + Rev int64 `json:"rev,omitempty"` +} + +type RsrcDetails struct { + Type string `json:"type,omitempty"` + Src string `json:"src,omitempty"` + C int64 `json:"c,omitempty"` + Tsrc string `json:"tsrc,omitempty"` + P string `json:"p,omitempty"` + M string `json:"m,omitempty"` +} + +func HandlePayload(payload interface{}, bootloaderConfig *BootLoaderConfig) error { + var data *Bootloader_HandlePayload + err := interfaceToStructJSON(&payload, &data) + if err != nil { + return err + } + + if data.CsrUpgrade != "" { + CsrBitmap = append(CsrBitmap, parseCSRBit(data.CsrUpgrade)...) + } + + if len(data.RsrcMap) > 0 { + for _, v := range data.RsrcMap { + shouldAdd := (bootloaderConfig.PhdOn && v.C == 2) || (!bootloaderConfig.PhdOn && v.C != 0) + if shouldAdd { + CsrBitmap = append(CsrBitmap, parseCSRBit(v.P)...) + } + } + } + + return nil +} + +// s always start with : +func parseCSRBit(s string) []int { + bits := make([]int, 0) + splitUp := strings.Split(s[1:], ",") + for _, b := range splitUp { + conv, err := strconv.ParseInt(b, 10, 32) + if err != nil { + log.Fatalf("failed to parse csrbit: %e", err) + } + if conv == 0 { + continue + } + bits = append(bits, int(conv)) + } + return bits +} \ No newline at end of file diff --git a/modules/config.go b/modules/config.go new file mode 100644 index 0000000..23e844a --- /dev/null +++ b/modules/config.go @@ -0,0 +1,28 @@ +package modules + +type SprinkleConfig struct { + ParamName string `json:"param_name,omitempty"` + ShouldRandomize bool `json:"should_randomize,omitempty"` + Version int `json:"version,omitempty"` +} + +type WebConnectionClassServerGuess struct { + ConnectionClass string `json:"connectionClass,omitempty"` +} + +type WebDevicePerfClassData struct { + DeviceLevel string `json:"deviceLevel,omitempty"` + YearClass any `json:"yearClass,omitempty"` +} + +type USIDMetadata struct { + BrowserID string `json:"browser_id,omitempty"` + PageID string `json:"page_id,omitempty"` + TabID string `json:"tab_id,omitempty"` + TransitionID int `json:"transition_id,omitempty"` + Version int `json:"version,omitempty"` +} + +type MessengerWebRegion struct { + Region string `json:"regionNullable,omitempty"` +} \ No newline at end of file diff --git a/modules/m.go b/modules/m.go new file mode 100644 index 0000000..ca1c63d --- /dev/null +++ b/modules/m.go @@ -0,0 +1,250 @@ +package modules + +import ( + "encoding/json" + "fmt" + "log" + "net/url" + "reflect" + "strings" + "github.com/0xzer/messagix/graphql" + "github.com/0xzer/messagix/lightspeed" +) + +var GraphQLData = &graphql.GraphQLTable{} +var VersionId int64 + +type lsSyncData struct { + Database int `json:"database,omitempty"` + EpochID int `json:"epoch_id,omitempty"` + LastAppliedCursor any `json:"last_applied_cursor,omitempty"` + SyncParams string `json:"sync_params,omitempty"` + Version int64 `json:"version,omitempty"` +} + +type EnvJSON struct { + UseTrustedTypes bool `json:"useTrustedTypes,omitempty"` + IsTrustedTypesReportOnly bool `json:"isTrustedTypesReportOnly,omitempty"` + RoutingNamespace string `json:"routing_namespace,omitempty"` + Ghlss string `json:"ghlss,omitempty"` + ScheduledCSSJSScheduler bool `json:"scheduledCSSJSScheduler,omitempty"` + UseFbtVirtualModules bool `json:"use_fbt_virtual_modules,omitempty"` + CompatIframeToken string `json:"compat_iframe_token,omitempty"` +} + +type Eqmc struct { + AjaxURL string `json:"u,omitempty"` + HasteSessionId string `json:"e,omitempty"` + S string `json:"s,omitempty"` + W int `json:"w,omitempty"` + FbDtsg string `json:"f,omitempty"` + L any `json:"l,omitempty"` +} + +type AjaxQueryParams struct { + A string `json:"__a"` + User string `json:"__user"` + CometReq string `json:"__comet_req"` + Jazoest string `json:"jazoest"` +} + +func (e *Eqmc) ParseAjaxURLData() (*AjaxQueryParams, error) { + u, err := url.Parse(e.AjaxURL) + if err != nil { + return nil, err + } + + params, err := url.ParseQuery(u.RawQuery) + if err != nil { + return nil, err + } + + var result AjaxQueryParams + + result.A = params.Get("__a") + result.User = params.Get("__user") + result.CometReq = params.Get("__comet_req") + result.Jazoest = params.Get("jazoest") + return &result, nil +} + +type JSON struct { + Eqmc Eqmc + EnvJSON EnvJSON +} + +var JSONData = JSON{} + +func HandleJSON(data []byte, id string) error { + var err error + switch id { + case "envjson": + var d EnvJSON + err = json.Unmarshal(data, &d) + JSONData.EnvJSON = d + case "__eqmc": + var d Eqmc + err = json.Unmarshal(data, &d) + JSONData.Eqmc = d + } + return err +} + +var CsrBitmap = make([]int, 0) +var Bitmap = make([]int, 0) + +func interfaceToStructJSON(data interface{}, i interface{}) error { + b, err := json.Marshal(&data) + if err != nil { + return err + } + + return json.Unmarshal(b, &i) +} + +func handleDefine(modName string, data []interface{}) error { + switch modName { + case "ssjs": + reflectedMs := reflect.ValueOf(&SchedulerJSDefined).Elem() + for _, child := range data { + configData := child.([]interface{}) + config := configData[2] + configName := configData[0].(string) + configId := int(configData[3].(float64)) + + if configId <= 0 { + continue + } + + Bitmap = append(Bitmap, configId) + field := reflectedMs.FieldByName(configName) + if !field.IsValid() { + //fmt.Printf("Invalid field name: %s\n", configName) + continue + } + if !field.CanSet() { + //fmt.Printf("Unsettable field: %s\n", configName) + continue + } + + err := interfaceToStructJSON(config, field.Addr().Interface()) + if err != nil { + return err + } + } + } + return nil +} + +func handleRequire(modName string, data []interface{}) error { + switch modName { + case "ssjs": + //reflectedMs := reflect.ValueOf(&SchedulerJSRequired).Elem() + for _, requireData := range data { + d := requireData.([]interface{}) + requireType := d[0].(string) + switch requireType { + case "CometPlatformRootClient": + moduleData := d[3].([]interface{}) + for _, v := range moduleData { + requestsMap, ok := v.([]interface{}) + if !ok { + continue + } + for _, req := range requestsMap { + var reqData *graphql.GraphQLPreloader + err := interfaceToStructJSON(req, &reqData) + if err != nil { + continue + } + if len(reqData.Variables.RequestPayload) > 0 { + var syncData *lsSyncData + err = json.Unmarshal([]byte(reqData.Variables.RequestPayload), &syncData) + if err != nil { + continue + } + VersionId = syncData.Version + } + } + + } + case "RelayPrefetchedStreamCache": + moduleData := d[3].([]interface{}) + //method := d[1].(string) + //dependencies := d[2].(string) + parserFunc := parseGraphMethodName(moduleData[0].(string)) + graphQLData := moduleData[1].(map[string]interface{}) + boxData, ok := graphQLData["__bbox"].(map[string]interface{}) + if !ok { + return fmt.Errorf("could not find __bbox in graphQLData map for parser func: %s", parserFunc) + } + + result, ok := boxData["result"] + if !ok { + return fmt.Errorf("could not find result in __bbox for parser func: %s", parserFunc) + } + + if parserFunc == "LSPlatformGraphQLLightspeedRequestQuery" { + handleLightSpeedQLRequest(result) + } else { + handleGraphQLData(parserFunc, result) + } + } + } + } + return nil +} + +func handleLightSpeedQLRequest(data interface{}) { + var lsData *graphql.LSPlatformGraphQLLightspeedRequestQuery + err := interfaceToStructJSON(&data, &lsData) + if err != nil { + log.Fatalf("failed to parse LightSpeedQLRequest data from html: %e", err) + } + + lsPayload := lsData.Data.Viewer.LightspeedWebRequest.Payload + dependencies := lightspeed.DependenciesToMap(lsData.Data.Viewer.LightspeedWebRequest.Dependencies) + decoder := lightspeed.NewLightSpeedDecoder(dependencies, SchedulerJSRequired.LSTable) + + var payload lightspeed.LightSpeedData + err = json.Unmarshal([]byte(lsPayload), &payload) + if err != nil { + log.Fatalf("failed to marshal lsPayload into LightSpeedData: %e", err) + } + + decoder.Decode(payload.Steps) +} + +func handleGraphQLData(name string, data interface{}) { + reflectedMs := reflect.ValueOf(GraphQLData).Elem() + dataField := reflectedMs.FieldByName(name) + if !dataField.IsValid() { + log.Println("Not handling GraphQLData for operation:", name) + return + } + + definition := dataField.Type().Elem() + newDefinition := reflect.New(definition).Interface() + + jsonBytes, err := json.Marshal(data) + if err != nil { + log.Println(fmt.Sprintf("failed to marshal GraphQL operation data %s", name)) + return + } + + err = json.Unmarshal(jsonBytes, newDefinition) + if err != nil { + log.Println(fmt.Sprintf("failed to unmarshal GraphQL operation data %s", name)) + return + } + + newSlice := reflect.Append(dataField, reflect.Indirect(reflect.ValueOf(newDefinition))) + dataField.Set(newSlice) +} + +func parseGraphMethodName(name string) string { + var s string + s = strings.Replace(name, "adp_", "", -1) + s = strings.Split(s, "RelayPreloader_")[0] + return s +} \ No newline at end of file diff --git a/modules/mqtt.go b/modules/mqtt.go new file mode 100644 index 0000000..0f01dc9 --- /dev/null +++ b/modules/mqtt.go @@ -0,0 +1,17 @@ +package modules + +type MqttWebConfig struct { + AppID int64 `json:"appID,omitempty"` + Capabilities int `json:"capabilities,omitempty"` + ChatVisibility bool `json:"chatVisibility,omitempty"` + ClientCapabilities int `json:"clientCapabilities,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Fbid string `json:"fbid,omitempty"` + HostNameOverride string `json:"hostNameOverride,omitempty"` + PollingEndpoint string `json:"pollingEndpoint,omitempty"` + SubscribedTopics []any `json:"subscribedTopics,omitempty"` +} + +type MqttWebDeviceID struct { + ClientID string `json:"clientID,omitempty"` +} \ No newline at end of file diff --git a/modules/scheduledserverjs.go b/modules/scheduledserverjs.go new file mode 100644 index 0000000..695893d --- /dev/null +++ b/modules/scheduledserverjs.go @@ -0,0 +1,69 @@ +package modules + +import ( + "log" + + "github.com/0xzer/messagix/table" +) + +type SchedulerJSDefine struct { + MqttWebConfig MqttWebConfig + MqttWebDeviceID MqttWebDeviceID + WebDevicePerfClassData WebDevicePerfClassData + BootloaderConfig BootLoaderConfig + CurrentBusinessUser CurrentBusinessAccount + SiteData SiteData + SprinkleConfig SprinkleConfig + USIDMetadata USIDMetadata + WebConnectionClassServerGuess WebConnectionClassServerGuess + MessengerWebRegion MessengerWebRegion + MessengerWebInitData MessengerWebInitData + LSD LSD + IntlViewerContext IntlViewerContext + IntlCurrentLocale IntlCurrentLocale + DTSGInitData DTSGInitData + DTSGInitialData DTSGInitialData + CurrentUserInitialData CurrentUserInitialData +} + +type SchedulerJSRequire struct { + LSTable *table.LSTable +} + +var SchedulerJSDefined = SchedulerJSDefine{} +var SchedulerJSRequired = SchedulerJSRequire{ + LSTable: &table.LSTable{}, +} + +func SSJSHandle(data interface{}) error { + box, ok := data.(map[string]interface{}) + if !ok { + _, ok := data.([]interface{}) + if ok { + return nil + } + log.Fatalf("failed to convert ssjs data to map[string]interface{}") + } + + var err error + for k, v := range box { + if v == nil { + continue + } + switch k { + case "__bbox": + boxMap := v.(map[string]interface{}) + for boxKey, boxData := range boxMap { + boxDataArr := boxData.([]interface{}) + switch boxKey { + case "require": + err = handleRequire("ssjs", boxDataArr) + continue + case "define": + err = handleDefine("ssjs", boxDataArr) + } + } + } + } + return err +} \ No newline at end of file diff --git a/modules/sitedata.go b/modules/sitedata.go new file mode 100644 index 0000000..8b9b252 --- /dev/null +++ b/modules/sitedata.go @@ -0,0 +1,31 @@ +package modules + +type SiteData struct { + SpinB string `json:"__spin_b,omitempty"` + SpinR int `json:"__spin_r,omitempty"` + SpinT int `json:"__spin_t,omitempty"` + BeOneAhead bool `json:"be_one_ahead,omitempty"` + BlHashVersion int `json:"bl_hash_version,omitempty"` + ClientRevision int `json:"client_revision,omitempty"` + CometEnv int `json:"comet_env,omitempty"` + HasteSession string `json:"haste_session,omitempty"` + HasteSite string `json:"haste_site,omitempty"` + Hsi string `json:"hsi,omitempty"` + IsComet bool `json:"is_comet,omitempty"` + IsExperimentalTier bool `json:"is_experimental_tier,omitempty"` + IsJitWarmedUp bool `json:"is_jit_warmed_up,omitempty"` + IsRtl bool `json:"is_rtl,omitempty"` + ManifestBaseURI string `json:"manifest_base_uri,omitempty"` + ManifestOrigin string `json:"manifest_origin,omitempty"` + ManifestVersionPrefix string `json:"manifest_version_prefix,omitempty"` + PkgCohort string `json:"pkg_cohort,omitempty"` + Pr int `json:"pr,omitempty"` + PushPhase string `json:"push_phase,omitempty"` + SemrHostBucket string `json:"semr_host_bucket,omitempty"` + ServerRevision int `json:"server_revision,omitempty"` + SkipRdBl bool `json:"skip_rd_bl,omitempty"` + Spin int `json:"spin,omitempty"` + Tier string `json:"tier,omitempty"` + Vip string `json:"vip,omitempty"` + WbloksEnv bool `json:"wbloks_env,omitempty"` +} \ No newline at end of file diff --git a/packets/connect.go b/packets/connect.go new file mode 100644 index 0000000..6f3baa0 --- /dev/null +++ b/packets/connect.go @@ -0,0 +1,27 @@ +package packets + +type ConnectPacket struct { + Packet byte +} + +func (p *ConnectPacket) Compress() byte { + return CONNECT << 4 +} + +func (p *ConnectPacket) Decompress(packetByte byte) error { + p.Packet = packetByte + return nil +} + +type ConnACKPacket struct { + Packet byte +} + +func (p *ConnACKPacket) Compress() byte { + return CONNACK << 4 +} + +func (p *ConnACKPacket) Decompress(packetByte byte) error { + p.Packet = packetByte + return nil +} \ No newline at end of file diff --git a/packets/packets.go b/packets/packets.go new file mode 100644 index 0000000..560ccc9 --- /dev/null +++ b/packets/packets.go @@ -0,0 +1,33 @@ +package packets + +const ( + CONNECT = 1 // CONNECT packet does not include any flags in the packet type + CONNACK = 2 // CONNECT ACKNOWLEDGEMENT packet does not include any flags in the packet type + + PUBLISH = 3 // PUBLISH packet without any flags + PUBACK = 4 // PUBLISH ACKNOWLEDGMENT packet + + SUBSCRIBE = 8 // SUBSCRIBE packet without any flags + SUBACK = 9 // SUBSCRIBE ACKNOWLEDGMENT packet +) + +const ( + DUP = 0x08 + RETAIN = 0x01 +) + +type QoS uint8 +func (q QoS) IsEnum() {} +func (q QoS) toByte() byte { + return byte(q & 0x03) +} +const ( + QOS_LEVEL_0 QoS = 0x00 + QOS_LEVEL_1 QoS = 0x01 + QOS_LEVEL_2 QoS = 0x02 +) + +type Packet interface{ + Compress() byte + Decompress(packetByte byte) error +} \ No newline at end of file diff --git a/packets/publish.go b/packets/publish.go new file mode 100644 index 0000000..55f59ef --- /dev/null +++ b/packets/publish.go @@ -0,0 +1,89 @@ +package packets + +import "fmt" + +// 00110010 +// 0011 +// 0010 +type PublishPacket struct { + packetType uint8 // packet type. (PUBLISH) + DUP uint8 // duplicate delivery flag (bit 3) - this is 1 or 0, 1 = is re-delivery/duplicate of message + QOSLevel QoS // quality of service level (bit 2 & 1) + RetainFlag uint8 // retain flag (bit 0) - if set to 1, the message should be retained by the broker and delivered to future subscribers with a matching subscription +} + +func (p *PublishPacket) GetPacketType() uint8 { + return p.packetType +} + +func (p *PublishPacket) Compress() byte { + var result byte = PUBLISH << 4 + if p.DUP == 1 { + result |= DUP + } + + result |= p.QOSLevel.toByte() << 1 + if p.RetainFlag == 1 { + result |= RETAIN + } + return result +} + +func (p *PublishPacket) Decompress(packetByte byte) error { + p.packetType = packetByte >> 4 + if p.packetType != PUBLISH { + return fmt.Errorf("tried to decompress publish packet type but result was not of PUBLISH packet type") + } + + if (packetByte & 0x08) != 0 { + p.DUP = 1 + } else { + p.DUP = 0 + } + + p.QOSLevel = QoS((packetByte & 0x06) >> 1) + + if (packetByte & 0x01) != 0 { + p.RetainFlag = 1 + } else { + p.RetainFlag = 0 + } + + return nil +} + +type PubACKPacket struct { + packetType uint8 // packet type (PUBACK) + packetID uint16 // The Packet Identifier +} + +func (p *PubACKPacket) Compress() byte {return 0} + +func (p *PubACKPacket) GetPacketType() uint8 { + return p.packetType +} + +// As PUBACK only consists of its type and packet ID, +// we don't have a "Compress" like with PUBLISH. +func (p *PubACKPacket) ToBytes() []byte { + return []byte{ + PUBACK << 4, + byte(p.packetID >> 8), + byte(p.packetID & 0xFF), + } +} + +// I doubt this will ever be used, but it might be. +func (p *PubACKPacket) Decompress(data []byte) error { + if len(data) != 3 { + return fmt.Errorf("invalid PUBACK packet length") + } + + p.packetType = data[0] >> 4 + if p.packetType != PUBACK { + return fmt.Errorf("expected PUBACK packet type but got %d", p.packetType) + } + + p.packetID = uint16(data[1])<<8 | uint16(data[2]) + return nil +} \ No newline at end of file diff --git a/payload.go b/payload.go new file mode 100644 index 0000000..a4eb3a7 --- /dev/null +++ b/payload.go @@ -0,0 +1,64 @@ +package messagix + +import ( + "github.com/0xzer/messagix/byter" + "github.com/0xzer/messagix/packets" +) + +type Payload interface { + Write() ([]byte, error) +} + +type ConnectPayload struct { + ProtocolName string `lengthType:"uint16"` + ProtocolLevel uint8 + ConnectFlags uint8 + KeepAliveTime uint16 + ClientId string `lengthType:"uint16"` + JSONData string `lengthType:"uint16"` +} + +func (cp *ConnectPayload) Write() ([]byte, error) { + return byter.NewWriter().WriteFromStruct(cp) +} + +func (c *Client) NewConnectRequest(jsonData string, connectFlags uint8) ([]byte, error) { + payload := &ConnectPayload{ + ProtocolName: c.configs.mqttConfig.ProtocolName, + ProtocolLevel: c.configs.mqttConfig.ProtocolLevel, + ConnectFlags: connectFlags, + KeepAliveTime: c.configs.mqttConfig.KeepAliveTimeout, + ClientId: c.configs.mqttConfig.ClientId, + JSONData: jsonData, + } + + packet := &packets.ConnectPacket{} + request := &Request{ + PacketByte: packet.Compress(), + } + return request.Write(payload) +} + +type PublishPayload struct { + Topic Topic `lengthType:"uint16"` + PacketId uint16 + JSONData string `lengthType:"uint16"` +} + + +func (pb *PublishPayload) Write() ([]byte, error) { + return byter.NewWriter().WriteFromStruct(pb) +} + +func (c *Client) NewPublishRequest(topic Topic, jsonData string, packetId uint16, packetByte byte) ([]byte, error) { + payload := &PublishPayload{ + Topic: topic, + PacketId: packetId, + JSONData: jsonData, + } + + request := &Request{ + PacketByte: packetByte, + } + return request.Write(payload) +} \ No newline at end of file diff --git a/request.go b/request.go new file mode 100644 index 0000000..f54fb13 --- /dev/null +++ b/request.go @@ -0,0 +1,23 @@ +package messagix + +import "github.com/0xzer/messagix/byter" + +type Request struct { + PacketByte uint8 + RemainingLength uint32 `vlq:"true"` +} + +func (r *Request) Write(payload Payload) ([]byte, error) { + payloadBytes, err := payload.Write() + if err != nil { + return nil, err + } + + r.RemainingLength = uint32(len(payloadBytes)) + header, err := byter.NewWriter().WriteFromStruct(r) + if err != nil { + return nil, err + } + + return append(header, payloadBytes...), nil +} \ No newline at end of file diff --git a/response.go b/response.go new file mode 100644 index 0000000..1022fae --- /dev/null +++ b/response.go @@ -0,0 +1,41 @@ +package messagix + +import ( + "fmt" + "log" + + "github.com/0xzer/messagix/byter" + "github.com/0xzer/messagix/packets" +) + +type ResponseData interface {} +type responseHandler func() (ResponseData) +var responseMap = map[uint8]responseHandler{ + packets.CONNACK: func() ResponseData {return &Event_Ready{}}, +} + +type Response struct { + PacketByte uint8 + RemainingLength uint32 `vlq:"true"` + ResponseData ResponseData +} + +func (r *Response) Read(data []byte) error { + log.Println(data) + reader := byter.NewReader(data) + err := reader.ReadToStruct(r) + if err != nil { + return err + } + + packetType := r.PacketByte >> 4 // parse the packet type by the leftmost 4 bits + responseHandlerFunc, ok := responseMap[packetType] + if !ok { + return fmt.Errorf("could not find response func handler for packet type %d", packetType) + } + r.ResponseData = responseHandlerFunc() + + offset := len(data) - reader.Buff.Len() + remainingData := data[offset:] + return byter.NewReader(remainingData).ReadToStruct(r.ResponseData) +} \ No newline at end of file diff --git a/socket.go b/socket.go new file mode 100644 index 0000000..997a7a5 --- /dev/null +++ b/socket.go @@ -0,0 +1,154 @@ +package messagix + +import ( + "errors" + "fmt" + "net/http" + + "github.com/0xzer/messagix/packets" + "github.com/gorilla/websocket" +) + +var ( + ErrSocketClosed = errors.New("socket is closed") + ErrSocketAlreadyOpen = errors.New("socket is already open") +) + +type Socket struct { + client *Client + conn *websocket.Conn + + packetsSent uint16 + + topics []Topic +} + +func (c *Client) NewSocketClient() *Socket { + return &Socket{ + client: c, + packetsSent: 0, + } +} + +func (s *Socket) Connect() error { + if s.conn != nil { + s.client.Logger.Err(ErrSocketAlreadyOpen).Msg("Failed to initialize connection to socket") + return ErrSocketAlreadyOpen + } + + headers := s.getConnHeaders() + brokerUrl := s.client.configs.mqttConfig.BuildBrokerUrl() + + s.client.Logger.Debug().Any("broker", brokerUrl).Msg("Dialing socket") + conn, _, err := websocket.DefaultDialer.Dial(brokerUrl, headers) + if err != nil { + return fmt.Errorf("failed to dial socket: %s", err.Error()) + } + + conn.SetCloseHandler(s.CloseHandler) + + s.conn = conn + + err = s.sendConnectPacket() + if err != nil { + return fmt.Errorf("failed to send CONNECT packet to socket: %s", err.Error()) + } + + go s.beginReadStream() + + appSettingPublishJSON, err := s.newAppSettingsPublishJSON() + if err != nil { + return err + } + + publishPacketByte := &packets.PublishPacket{ + QOSLevel: packets.QOS_LEVEL_1, + } + + appSettingPublishPayload, err := s.client.NewPublishRequest(APP_SETTINGS, appSettingPublishJSON, s.packetsSent+1, publishPacketByte.Compress()) + if err != nil { + return err + } + + err = s.sendData(appSettingPublishPayload) + if err != nil { + return err + } + return nil +} + +func (s *Socket) beginReadStream() { + for { + messageType, p, err := s.conn.ReadMessage() + if err != nil { + s.handleErrorEvent(fmt.Errorf("error reading message from websocket: %s", err.Error())) + return + } + + switch messageType { + case websocket.TextMessage: + s.client.Logger.Debug().Any("data", p).Bytes("bytes", p).Msg("Received TextMessage") + case websocket.BinaryMessage: + s.handleBinaryMessage(p) + } + } +} + +func (s *Socket) sendData(data []byte) error { + s.client.Logger.Debug().Any("data", data).Hex("hex", data).Msg("Sending data to socket") + + packetType := data[0] >> 4 + if packetType == packets.PUBLISH { + s.packetsSent += 1 + } + + err := s.conn.WriteMessage(websocket.BinaryMessage, data) + if err != nil { + e := fmt.Errorf("error sending data to websocket: %s", err.Error()) + s.handleErrorEvent(e) + return e + } + return nil +} + +func (s *Socket) sendConnectPacket() error { + connectAdditionalData, err := s.newConnectJSON() + if err != nil { + return err + } + + connectFlags := CreateConnectFlagByte(ConnectFlags{CleanSession: true, Username: true}) + connectPayload, err := s.client.NewConnectRequest(connectAdditionalData, connectFlags) + if err != nil { + return err + } + return s.sendData(connectPayload) +} + +func (s *Socket) CloseHandler(code int, text string) error { + s.conn = nil + + if s.client.eventHandler != nil { + socketCloseEvent := &Event_SocketClosed{ + Code: code, + Text: text, + } + s.client.eventHandler(socketCloseEvent) + } + + return nil +} + +func (s *Socket) setTopics(topics []Topic) { + s.topics = topics +} + +func (s *Socket) getConnHeaders() http.Header { + h := http.Header{} + + h.Add("cookie", s.client.cookies.ToString()) + h.Add("user-agent", USER_AGENT) + h.Add("origin", "https://www.facebook.com") + + return h +} \ No newline at end of file diff --git a/table/debug.go b/table/debug.go new file mode 100644 index 0000000..0e523fd --- /dev/null +++ b/table/debug.go @@ -0,0 +1,43 @@ +package table + +type LSMciTraceLog struct { + SomeInt0 int64 `index:"0"` + MCITraceUnsampledEventTraceId string `index:"1"` + Unknown2 interface{} `index:"2"` + SomeInt3 int64 `index:"3"` + Unknown4 interface{} `index:"4"` + DatascriptExecute string `index:"5"` + SomeInt6 int64 `index:"6"` +} + +type LSExecuteFirstBlockForSyncTransaction struct { + DatabaseId int64 `index:"0"` + EpochId int64 `index:"1"` + CurrentCursor string `index:"3"` + SyncStatus int64 `index:"4"` + SendSyncParams bool `index:"5"` + MinTimeToSyncTimestampMs int64 `index:"6"` // fix this, use conditionIndex + CanIgnoreTimestamp bool `index:"7"` + SyncChannel int64 `index:"8"` +} + +type LSExecuteFinallyBlockForSyncTransaction struct { + Unknown0 bool `index:"0"` + Unknown1 int64 `index:"1"` + Unknown2 int64 `index:"2"` +} + +type LSSetHMPSStatus struct { + AccountId int64 `index:"0"` + Unknown1 int64 `index:"1"` + Timestamp int64 `index:"2"` +} + +type LSUpsertSequenceId struct { + LastAppliedMailboxSequenceId int64 `index:"0"` +} + +type LSSetRegionHint struct { + Unknown0 int64 `index:"0"` + RegionHint string `index:"1"` +} \ No newline at end of file diff --git a/table/enums.go b/table/enums.go new file mode 100644 index 0000000..65731a2 --- /dev/null +++ b/table/enums.go @@ -0,0 +1,31 @@ +package table + +type DisplayedContentTypes int64 +const ( + TEXT DisplayedContentTypes = 1 +) + +type Gender int64 +const ( + NOT_A_PERSON Gender = 0 + FEMALE_SINGULAR Gender = 1 + MALE_SINGULAR Gender = 2 + FEMALE_SINGULAR_GUESS Gender = 3 + MALE_SINGULAR_GUESS Gender = 4 + MIXED_UNKNOWN Gender = 5 + NEUTER_SINGULAR Gender = 6 + UNKNOWN_SINGULAR Gender = 7 + FEMALE_PLURAL Gender = 8 + MALE_PLURAL Gender = 9 + NEUTER_PLURAL Gender = 10 + UNKNOWN_PLURAL Gender = 11 +) + +type ContactViewerRelationship int64 +const ( + UNKNOWN_RELATIONSHIP ContactViewerRelationship = 0 + NOT_CONTACT ContactViewerRelationship = 1 + CONTACT_OF_VIEWER ContactViewerRelationship = 2 + FACEBOOK_FRIEND ContactViewerRelationship = 3 + SOFT_CONTACT ContactViewerRelationship = 4 +) \ No newline at end of file diff --git a/table/folders.go b/table/folders.go new file mode 100644 index 0000000..31d974e --- /dev/null +++ b/table/folders.go @@ -0,0 +1,6 @@ +package table + +type LSUpsertFolderSeenTimestamp struct { + ParentThreadKey int64 `index:"0"` + LastSeenRequestTimestampMs int64 `index:"1"` +} \ No newline at end of file diff --git a/table/messages.go b/table/messages.go new file mode 100644 index 0000000..21b8732 --- /dev/null +++ b/table/messages.go @@ -0,0 +1,105 @@ +package table + +/* + Instructs the client to clear pinned messages (delete by ThreadKey) +*/ +type LSClearPinnedMessages struct { + ThreadKey int64 `index:"0"` +} + +type LSUpsertMessage struct { + Text string `index:"0"` + SubscriptErrorMessage string `index:"1"` + AuthorityLevel int64 `index:"2"` + ThreadKey int64 `index:"3"` + TimestampMs int64 `index:"5"` + PrimarySortKey int64 `index:"6"` + SecondarySortKey int64 `index:"7"` + MessageId string `index:"8"` + OfflineThreadingId string `index:"9"` + SenderId int64 `index:"10"` + StickerId int64 `index:"11"` + IsAdminMessage bool `index:"12"` + MessageRenderingType int64 `index:"13"` + SendStatus int64 `index:"15"` + SendStatusV2 int64 `index:"16"` + IsUnsent bool `index:"17"` + UnsentTimestampMs int64 `index:"18"` + MentionOffsets int64 `index:"19"` + MentionLengths int64 `index:"20"` + MentionIds int64 `index:"21"` + MentionTypes int64 `index:"22"` + ReplySourceId int64 `index:"23"` + ReplySourceType int64 `index:"24"` + ReplySourceTypeV2 int64 `index:"25"` + ReplyStatus int64 `index:"26"` + ReplySnippet string `index:"27"` + ReplyMessageText string `index:"28"` + ReplyToUserId int64 `index:"29"` + ReplyMediaExpirationTimestampMs int64 `index:"30"` + ReplyMediaUrl string `index:"31"` + ReplyMediaPreviewWidth int64 `index:"33"` + ReplyMediaPreviewHeight int64 `index:"34"` + ReplyMediaUrlMimeType int64 `index:"35"` + ReplyMediaUrlFallback string `index:"36"` + ReplyCtaId int64 `index:"37"` + ReplyCtaTitle string `index:"38"` + ReplyAttachmentType int64 `index:"39"` + ReplyAttachmentId int64 `index:"40"` + ReplyAttachmentExtra int64 `index:"41"` + ReplyType int64 `index:"42"` + IsForwarded bool `index:"43"` + ForwardScore int64 `index:"44"` + HasQuickReplies bool `index:"45"` + AdminMsgCtaId int64 `index:"46"` + AdminMsgCtaTitle int64 `index:"47"` + AdminMsgCtaType int64 `index:"48"` + CannotUnsendReason int64 `index:"49"` + TextHasLinks bool `index:"50"` + ViewFlags int64 `index:"51"` + DisplayedContentTypes DisplayedContentTypes `index:"52"` + ViewedPluginKey int64 `index:"53"` + ViewedPluginContext int64 `index:"54"` + QuickReplyType int64 `index:"55"` + HotEmojiSize int64 `index:"56"` + ReplySourceTimestampMs int64 `index:"57"` + EphemeralDurationInSec int64 `index:"58"` + MsUntilExpirationTs int64 `index:"59"` + EphemeralExpirationTs int64 `index:"60"` + TakedownState int64 `index:"61"` + IsCollapsed bool `index:"62"` + SubthreadKey int64 `index:"63"` +} + +type LSSetForwardScore struct { + ThreadKey int64 `index:"0"` + MessageId string `index:"1"` + TimestampMs int64 `index:"2"` + ForwardScore int64 `index:"3"` +} + +type LSSetMessageDisplayedContentTypes struct { + ThreadKey int64 `index:"0"` + MessageId string `index:"1"` + TimestampMs int64 `index:"2"` + Text string `index:"3"` + Calc1 bool `index:"4"` + Calc2 bool `index:"5"` +} + +type LSInsertNewMessageRange struct { + ThreadKey int64 `index:"0"` + MinTimestampMsTemplate int64 `index:"1"` + MaxTimestampMsTemplate int64 `index:"2"` + MinMessageId string `index:"3"` + MaxMessageId string `index:"4"` + MaxTimestampMs int64 `index:"5"` + MinTimestampMs int64 `index:"6"` + HasMoreBefore bool `index:"7"` + HasMoreAfter bool `index:"8"` + Unknown interface{} `index:"9"` +} + +type LSDeleteExistingMessageRanges struct { + ConsistentThreadFbid int64 `index:"0"` +} \ No newline at end of file diff --git a/table/sync_groups.go b/table/sync_groups.go new file mode 100644 index 0000000..ef64d2e --- /dev/null +++ b/table/sync_groups.go @@ -0,0 +1,18 @@ +package table + +type LSTruncateTablesForSyncGroup struct { + SyncGroup int64 `index:"0"` +} + +type LSTruncateThreadRangeTablesForSyncGroup struct { + ParentThreadKey int64 `index:"0"` +} + +type LSUpsertSyncGroupThreadsRange struct { + SyncGroup int64 `index:"0"` + ParentThreadKey int64 `index:"1"` + MinLastActivityTimestampMs int64 `index:"2"` + HasMoreBefore bool `index:"3"` + IsLoadingBefore bool `index:"4"` + MinThreadKey int64 `index:"5"` +} \ No newline at end of file diff --git a/table/table.go b/table/table.go new file mode 100644 index 0000000..14404ef --- /dev/null +++ b/table/table.go @@ -0,0 +1,33 @@ +package table + +/* + Unknown fields = I don't know what type it is supposed to be, because I only see 9 which is undefined +*/ + +type LSTable struct { + LSMciTraceLog []LSMciTraceLog + LSExecuteFirstBlockForSyncTransaction []LSExecuteFirstBlockForSyncTransaction + LSTruncateMetadataThreads []LSTruncateMetadataThreads + LSTruncateThreadRangeTablesForSyncGroup []LSTruncateThreadRangeTablesForSyncGroup + LSUpsertSyncGroupThreadsRange []LSUpsertSyncGroupThreadsRange + LSUpsertInboxThreadsRange []LSUpsertInboxThreadsRange + LSUpdateThreadsRangesV2 []LSUpdateThreadsRangesV2 + LSUpsertFolderSeenTimestamp []LSUpsertFolderSeenTimestamp + LSSetHMPSStatus []LSSetHMPSStatus + LSTruncateTablesForSyncGroup []LSTruncateTablesForSyncGroup + LSDeleteThenInsertThread []LSDeleteThenInsertThread + LSAddParticipantIdToGroupThread []LSAddParticipantIdToGroupThread + LSClearPinnedMessages []LSClearPinnedMessages + LSWriteThreadCapabilities []LSWriteThreadCapabilities + LSUpsertMessage []LSUpsertMessage + LSSetForwardScore []LSSetForwardScore + LSSetMessageDisplayedContentTypes []LSSetMessageDisplayedContentTypes + LSUpdateReadReceipt []LSUpdateReadReceipt + LSInsertNewMessageRange []LSInsertNewMessageRange + LSDeleteExistingMessageRanges []LSDeleteExistingMessageRanges + LSUpsertSequenceId []LSUpsertSequenceId + LSVerifyContactRowExists []LSVerifyContactRowExists + LSThreadsRangesQuery []LSThreadsRangesQuery + LSSetRegionHint []LSSetRegionHint + LSExecuteFinallyBlockForSyncTransaction []LSExecuteFinallyBlockForSyncTransaction +} \ No newline at end of file diff --git a/table/threads.go b/table/threads.go new file mode 100644 index 0000000..05f21b8 --- /dev/null +++ b/table/threads.go @@ -0,0 +1,141 @@ +package table + +type LSTruncateMetadataThreads struct {} + +type LSUpsertInboxThreadsRange struct { + SyncGroup int64 `index:"0"` + MinLastActivityTimestampMs int64 `index:"1"` + HasMoreBefore bool `index:"2"` + IsLoadingBefore bool `index:"3"` + MinThreadKey int64 `index:"4"` +} + +type LSUpdateThreadsRangesV2 struct { + FolderName string `index:"0"` + ParentThreadKey int64 `index:"1"` /* not sure */ + MinLastActivityTimestampMs int64 `index:"2"` + MinThreadKey int64 `index:"3"` + IsLoadingBefore int64 `index:"4"` /* not sure */ +} + +type LSDeleteThenInsertThread struct { + LastActivityTimestampMs int64 `index:"0"` + LastReadWatermarkTimestampMs int64 `index:"1"` + Snippet string `index:"2"` + ThreadName string `index:"3"` + ThreadPictureUrl string `index:"4"` + NeedsAdminApprovalForNewParticipant bool `index:"5"` + AuthorityLevel int64 `index:"6"` + ThreadKey int64 `index:"7"` + MailboxType int64 `index:"8"` + ThreadType int64 `index:"9"` + FolderName string `index:"10"` + ThreadPictureUrlFallback string `index:"11"` + ThreadPictureUrlExpirationTimestampMs int64 `index:"12"` + RemoveWatermarkTimestampMs int64 `index:"13"` + MuteExpireTimeMs int64 `index:"14"` + MuteCallsExpireTimeMs int64 `index:"15"` + GroupNotificationSettings int64 `index:"16"` + IsAdminSnippet bool `index:"17"` + SnippetSenderContactId int64 `index:"18"` + SnippetStringHash string `index:"21"` + SnippetStringArgument1 string `index:"22"` + SnippetAttribution int64 `index:"23"` + SnippetAttributionStringHash string `index:"24"` + DisappearingSettingTtl int64 `index:"25"` + DisappearingSettingUpdatedTs int64 `index:"26"` + DisappearingSettingUpdatedBy int64 `index:"27"` + OngoingCallState int64 `index:"29"` + CannotReplyReason int64 `index:"30"` + CustomEmoji int64 `index:"31"` + CustomEmojiImageUrl string `index:"32"` + OutgoingBubbleColor int64 `index:"33"` + ThemeFbid int64 `index:"34"` + ParentThreadKey int64 `index:"35"` + NullstateDescriptionText1 string `index:"36"` + NullstateDescriptionType1 int64 `index:"37"` + NullstateDescriptionText2 string `index:"38"` + NullstateDescriptionType2 int64 `index:"39"` + NullstateDescriptionText3 string `index:"40"` + NullstateDescriptionType3 int64 `index:"41"` + DraftMessage string `index:"42"` + SnippetHasEmoji bool `index:"43"` + HasPersistentMenu bool `index:"44"` + DisableComposerInput bool `index:"45"` + CannotUnsendReason int64 `index:"46"` + ViewedPluginKey int64 `index:"47"` + ViewedPluginContext int64 `index:"48"` + ClientThreadKey int64 `index:"49"` + Capabilities int64 `index:"50"` + ShouldRoundThreadPicture bool `index:"51"` + ProactiveWarningDismissTime int64 `index:"52"` + IsCustomThreadPicture bool `index:"53"` + OtidOfFirstMessage int64 `index:"54"` + NormalizedSearchTerms int64 `index:"55"` + AdditionalThreadContext int64 `index:"56"` + DisappearingThreadKey int64 `index:"57"` + IsDisappearingMode bool `index:"58"` + DisappearingModeInitiator int64 `index:"59"` + UnreadDisappearingMessageCount int64 `index:"60"` + LastMessageCtaId int64 `index:"62"` + LastMessageCtaType int64 `index:"63"` + ConsistentThreadFbid int64 `index:"64"` + ThreadDescription int64 `index:"65"` + UnsendLimitMs int64 `index:"66"` + SyncGroup int64 `index:"67"` + ThreadInvitesEnabled int64 `index:"68"` + ThreadInviteLink string `index:"69"` + NumUnreadSubthreads int64 `index:"70"` + SubthreadCount int64 `index:"71"` + ThreadInvitesEnabledV2 int64 `index:"72"` + EventStartTimestampMs int64 `index:"73"` + EventEndTimestampMs int64 `index:"74"` + TakedownState int64 `index:"75"` + MemberCount int64 `index:"76"` + SecondaryParentThreadKey int64 `index:"77"` + IgFolder int64 `index:"78"` + InviterId int64 `index:"79"` + ThreadTags int64 `index:"80"` + ThreadStatus int64 `index:"81"` + ThreadSubtype int64 `index:"82"` + PauseThreadTimestamp int64 `index:"83"` +} + +type LSAddParticipantIdToGroupThread struct { + ThreadKey int64 `index:"0"` + ContactId int64 `index:"1"` + ReadWatermarkTimestampMs int64 `index:"2"` + ReadActionTimestampMs int64 `index:"3"` + DeliveredWatermarkTimestampMs int64 `index:"4"` + Nickname string `index:"5"` + IsAdmin bool `index:"6"` + SubscribeSource int64 `index:"7"` + AuthorityLevel int64 `index:"9"` + NormalizedSearchTerms int64 `index:"10"` + IsSuperAdmin bool `index:"11"` + ThreadRoles int64 `index:"12"` +} + +type LSWriteThreadCapabilities struct { + ThreadKey int64 `index:"0"` + Capabilities int64 `index:"1"` + Capabilities2 int64 `index:"2"` + Capabilities3 int64 `index:"3"` +} + +type LSUpdateReadReceipt struct { + ReadWatermarkTimestampMs int64 `index:"0"` + ThreadKey int64 `index:"1"` + ContactId int64 `index:"2"` + ReadActionTimestampMs int64 `index:"3"` +} + +type LSThreadsRangesQuery struct { + ParentThreadKey int64 `index:"0"` + Unknown1 bool `index:"1"` + IsAfter bool `index:"2"` + ReferenceThreadKey int64 `conditionField:"IsAfter" indexes:"4,3"` + ReferenceActivityTimestamp int64 `conditionField:"IsAfter" indexes:"5,6"` + AdditionalPagesToFetch int64 `index:"7"` + Unknown8 bool `index:"8"` +} \ No newline at end of file diff --git a/table/user.go b/table/user.go new file mode 100644 index 0000000..4d846e1 --- /dev/null +++ b/table/user.go @@ -0,0 +1,22 @@ +package table + + +type LSVerifyContactRowExists struct { + ContactId int64 `index:"0"` + Unknown1 int64 `index:"1"` + ProfilePictureUrl string `index:"2"` + Name string `index:"3"` + ContactType int64 `index:"4"` + ProfilePictureFallbackUrl string `index:"5"` + Unknown6 int64 `index:"6"` + Unknown7 int64 `index:"7"` + IsMemorialized bool `index:"9"` + BlockedByViewerStatus int64 `index:"11"` + CanViewerMessage bool `index:"12"` + AuthorityLevel int64 `index:"14"` + Capabilities int64 `index:"15"` + Capabilities2 int64 `index:"16"` + Gender Gender `index:"18"` + ContactViewerRelationship ContactViewerRelationship `index:"19"` + SecondaryName string `index:"20"` +} \ No newline at end of file diff --git a/task.go b/task.go new file mode 100644 index 0000000..e41b677 --- /dev/null +++ b/task.go @@ -0,0 +1,75 @@ +package messagix + +import ( + "github.com/0xzer/messagix/methods" +) + +type TaskData struct { + FailureCount interface{} `json:"failure_count,omitempty"` + Label string `json:"label,omitempty"` + Payload interface{} `json:"payload,omitempty"` + QueueName interface{} `json:"queue_name,omitempty"` + TaskId int64 `json:"task_id,omitempty"` +} + +type TaskPayload struct { + EpochId int64 `json:"epoch_id,omitempty"` + Tasks []TaskData `json:"tasks,omitempty"` + VersionId int64 `json:"version_id,omitempty"` +} + +type TaskManager struct { + client *Client + activeTaskIds []int + currTasks []TaskData +} + +func (tm *TaskManager) FinalizePayload() *TaskPayload { + p := &TaskPayload{ + EpochId: methods.GenerateEpochId(), + Tasks: tm.currTasks, + VersionId: tm.client.configs.siteConfig.VersionId, + } + tm.currTasks = make([]TaskData, 0) + return p +} + +func (tm *TaskManager) AddNewTask(task Task) { + payload, queueName := task.create() + label := task.getLabel() + tm.client.Logger.Debug().Any("label", label).Any("payload", payload).Any("queueName", queueName).Msg("Creating task") + taskData := TaskData{ + FailureCount: nil, + Label: label, + Payload: payload, + QueueName: queueName, + TaskId: tm.GetTaskId(), + } + + tm.currTasks = append(tm.currTasks, taskData) +} + +func (tm *TaskManager) GetTaskId() int64 { + if len(tm.currTasks) == 0 { + return 0 + } + return int64(len(tm.currTasks)) +} + +type Task interface { + getLabel() string + create() (interface{}, interface{}) // payload, queue_name +} + +type GetContactsTask struct { + Limit int64 `json:"limit,omitempty"` +} + +func (t *GetContactsTask) getLabel() string { + return "452" +} + +func (t *GetContactsTask) create() (interface{}, interface{}) { + queueName := []string{"search_contacts", methods.GenerateTimestampString()} + return t.Limit, queueName +} \ No newline at end of file diff --git a/topics.go b/topics.go new file mode 100644 index 0000000..48b856e --- /dev/null +++ b/topics.go @@ -0,0 +1,25 @@ +package messagix + +import "encoding/json" + +type Topic string + +const ( + UNKNOWN_TOPIC Topic = "unknown" + APP_SETTINGS Topic = "/ls_app_settings" +) + +type AppSettingsPublish struct { + LsFdid string `json:"ls_fdid"` + SchemaVersion string `json:"ls_sv"` /* unsure if this is language-code based (but it is 6512074225573706 all the time)*/ +} + +func (s *Socket) newAppSettingsPublishJSON() (string, error) { + payload := &AppSettingsPublish{ + LsFdid: "", + SchemaVersion: "6512074225573706", + } + + jsonData, err := json.Marshal(payload) + return string(jsonData), err +} \ No newline at end of file diff --git a/types/bootloader.go b/types/bootloader.go new file mode 100644 index 0000000..0825d7e --- /dev/null +++ b/types/bootloader.go @@ -0,0 +1,20 @@ +package types + +type HandlePayload struct { + Consistency Consistency `json:"consistency,omitempty"` + RsrcMap map[string]RsrcDetails `json:"rsrcMap,omitempty"` + CsrUpgrade string `json:"csrUpgrade,omitempty"` +} + +type Consistency struct { + Rev int64 `json:"rev,omitempty"` +} + +type RsrcDetails struct { + Type string `json:"type,omitempty"` + Src string `json:"src,omitempty"` + C int64 `json:"c,omitempty"` + Tsrc string `json:"tsrc,omitempty"` + P string `json:"p,omitempty"` + M string `json:"m,omitempty"` +} \ No newline at end of file diff --git a/types/configs.go b/types/configs.go new file mode 100644 index 0000000..c9de2ae --- /dev/null +++ b/types/configs.go @@ -0,0 +1,94 @@ +package types + +import ( + "net/url" + "strconv" + + "github.com/0xzer/messagix/crypto" +) + +type MqttWebDeviceID struct { + ClientID string `json:"clientID,omitempty"` +} + +type MQTTWebConfig struct { + Fbid string `json:"fbid,omitempty"` + AppID int64 `json:"appID,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + PollingEndpoint string `json:"pollingEndpoint,omitempty"` + SubscribedTopics []any `json:"subscribedTopics,omitempty"` + Capabilities int `json:"capabilities,omitempty"` + ClientCapabilities int `json:"clientCapabilities,omitempty"` + ChatVisibility bool `json:"chatVisibility,omitempty"` + HostNameOverride string `json:"hostNameOverride,omitempty"` +} + +type MQTTConfig struct { + ProtocolName string + ProtocolLevel uint8 + ClientId string + Broker string + KeepAliveTimeout uint16 + SessionId int64 + AppId int64 + ClientCapabilities int + Capabilities int + ChatOn bool + SubscribedTopics []any + ConnectionType string + HostNameOverride string + Cid string +} + +func (m *MQTTConfig) BuildBrokerUrl() string { + query := &url.Values{} + query.Add("cid", m.Cid) + query.Add("sid", strconv.Itoa(int(m.SessionId))) + + return m.Broker + "&" + query.Encode() +} + +type GraphQLPayload struct { + Av string `json:"av,omitempty"` // user id + User string `json:"__user,omitempty"` // user id + A string `json:"__a,omitempty"` // always 1? + ReqId string `json:"__req,omitempty"` + HasteSession string `json:"__hs,omitempty"` + Pr string `json:"dpr,omitempty"` + ConnectionClass string `json:"__ccg,omitempty"` + Revision string `json:"__rev,omitempty"` + WebSessionId string `json:"__s,omitempty"` + HasteSessionId string `json:"__hsi,omitempty"` + CompressedBitmap string `json:"__dyn,omitempty"` + CompressedCsrBitmap string `json:"__csr,omitempty"` + CometReq string `json:"__comet_req,omitempty"` + FbDtsg string `json:"fb_dtsg,omitempty"` + Jazoest string `json:"jazoest,omitempty"` + LsdToken string `json:"lsd,omitempty"` + SpinR string `json:"__spin_r,omitempty"` + SpinB string `json:"__spin_b,omitempty"` + SpinT string `json:"__spin_t,omitempty"` + FbAPICallerClass string `json:"fb_api_caller_class,omitempty"` + FbAPIReqFriendlyName string `json:"fb_api_req_friendly_name,omitempty"` + Variables interface{} `json:"variables,omitempty"` + ServerTimestamps bool `json:"server_timestamps,omitempty"` // "true" or "false" + DocID string `json:"doc_id,omitempty"` +} + +type SiteConfig struct { + Bitmap *crypto.Bitmap + CSRBitmap *crypto.Bitmap + HasteSessionId string + WebSessionId string + CometReq string + LsdToken string + SpinT string + SpinB string + SpinR string + FbDtsg string + Jazoest string + Pr string + HasteSession string + ConnectionClass string + VersionId int64 +} \ No newline at end of file diff --git a/types/contenttypes.go b/types/contenttypes.go new file mode 100644 index 0000000..918a14d --- /dev/null +++ b/types/contenttypes.go @@ -0,0 +1,7 @@ +package types + +type ContentType string +const ( + JSON ContentType = "application/json" + FORM ContentType = "application/x-www-form-urlencoded" +) \ No newline at end of file diff --git a/types/cookies.go b/types/cookies.go new file mode 100644 index 0000000..acd8728 --- /dev/null +++ b/types/cookies.go @@ -0,0 +1,94 @@ +package types + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" +) + +type Cookies struct { + Datr string `json:"datr,omitempty"` + Sb string `json:"sb,omitempty"` + AccountId string `json:"c_user,omitempty"` + Xs string `json:"xs,omitempty"` + Fr string `json:"fr,omitempty"` + Wd string `json:"wd,omitempty"` + Presence string `json:"presence,omitempty"` +} + +func (c *Cookies) ToString() string { + s := "" + values := reflect.ValueOf(*c) + for i := 0; i < values.NumField(); i++ { + field := values.Type().Field(i) + value := values.Field(i).Interface() + + zeroValue := reflect.Zero(field.Type).Interface() + if value == zeroValue { + continue + } + + tagValue := field.Tag.Get("json") + tagName := strings.Split(tagValue, ",")[0] + s += fmt.Sprintf("%s=%v; ", tagName, value) + } + + return s +} + +// FROM JSON FILE. +func NewCookiesFromFile(path string) (*Cookies, error) { + jsonBytes, jsonErr := os.ReadFile(path) + if jsonErr != nil { + return nil, jsonErr + } + + session := &Cookies{} + + marshalErr := json.Unmarshal(jsonBytes, session) + if marshalErr != nil { + return nil, marshalErr + } + + return session, nil +} + + +func NewCookiesFromString(cookieStr string) *Cookies { + datr := extractCookieValue(cookieStr, "datr") + sb := extractCookieValue(cookieStr, "sb") + accountId := extractCookieValue(cookieStr, "c_user") + xs := extractCookieValue(cookieStr, "xs") + fr := extractCookieValue(cookieStr, "fr") + wd := extractCookieValue(cookieStr, "wd") + presence := extractCookieValue(cookieStr, "presence") + + return &Cookies{ + Datr: datr, + Sb: sb, + AccountId: accountId, + Xs: xs, + Fr: fr, + Wd: wd, + Presence: presence, + } +} + + + +func extractCookieValue(cookieString, key string) string { + startIndex := strings.Index(cookieString, key) + if startIndex == -1 { + return "" + } + + startIndex += len(key) + 1 + endIndex := strings.IndexByte(cookieString[startIndex:], ';') + if endIndex == -1 { + return cookieString[startIndex:] + } + + return cookieString[startIndex : startIndex+endIndex] +} \ No newline at end of file