Wstęp

Poniższa implementacja opiera się o opis protokołu GG. Nie będę omawiać dokładnie struktury każdego pakietu. Szsczegółowy opis i wyjaśnienia co, jak i dlaczego, znajdują się na stronie opisu protokołu.

Własna implementacja bota GG może przydać się w autorskich aplikacjach np. do powiadamiania o zdarzeniach/zamówieniach itd. Może również posłużyć do masowego rozsyłania spamu :). Jeśli jesteś zainteresowany gotową aplikacją to napisz do mnie.

Z braku czasu i chęci, nie zaimplementowałem w całości wszystkich funkcjonalności protokołu (patrz sekcja funkcjonalność). Brakuje między innymi przesyłania sformatowanych wiadomości. Jest jednak dobrą bazą do dalszego rozwoju i napisania własnej pełnej implementacji.

Spis treści

Funkcjonalność

Zaimplementowane zostały następujące funkcjonalności protokołu GG:

  • Logowanie/wylogowanie (nowy klient GG >= 8.0)
  • Zmiana statusu i opisu
  • Dodawanie/usuwanie/blokowanie kontaktów
  • Powiadomienia o statusie znajomych
  • Odbieranie rozszerzonych informacji o znajomych
  • Pisanie/odbieranie wiadomości w HTML i w plain tekście
  • Odbieranie/powiadamianie o postępie w pisaniu
  • Pobieranie/aktualizacja/szukanie w katalogu publicznym

Komunikacja TCP/IP

Komunikacja pomiędzy klientem a serwerem GG odbywa się poprzez stos protokołów TCP/IP. W tej sekcji napiszemy klasę odpowiedzialną za zarządzanie połączeniem TCP i przesyłaniem danych. Klasa ta jest niezależna od sposobu implementacji protokołu GG.

Implementacja

Utwórz folder TCP w głownym katalogu projekt a w nim klasę TCPConnector.

TCPConnector.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
 
namespace MTGG.TCP
{
    internal class TCPConnector
    {
        public TCPConnector() { }
 
        public void Open(string host, int port)
        {
            if (!this.startConnect)
            {
                this.tcp = new TcpClient();
                this.startConnect = true;
                this.tcp.BeginConnect(host, port, ConnectCallback, null);
            }
        }
 
        public void Close()
        {
            this.Disconnected(this, null);
            this.startConnect = false;
            this.stream.Close();
            this.tcp.Close();
        }
 
        public void Write(byte[] bytes)
        {
            this.stream.BeginWrite(bytes, 0, bytes.Length, WriteCallback, null);
        }
 
        public event EventHandler Connected;
        public event EventHandler Disconnected;
        public event ExceptionEventHandler Error;
        public event DataEventHandler DataReceived;
 
        public bool IsOpen
        {
            get { return this.tcp != null && this.tcp.Connected; }
        }
 
        private void ConnectCallback(IAsyncResult result)
        {
            try
            {
                this.tcp.EndConnect(result);
            }
            catch (Exception ex)
            {
                this.Error(this, new ExceptionEventArgs(ex));
                return;
            }
 
            this.stream = this.tcp.GetStream();
            this.Connected(this, null);
            byte[] buffer = new byte[this.tcp.ReceiveBufferSize];
            this.stream.BeginRead(buffer, 0, buffer.Length, ReadCallback, buffer);
        }
 
        private void ReadCallback(IAsyncResult result)
        {
            int read;
            try
            {
                read = this.stream.EndRead(result);
            }
            catch (Exception ex)
            {
                this.Error(this, new ExceptionEventArgs(ex));
                this.Close();
                return;
            }
 
            if (read == 0)
            {
                return;
            }
 
            byte[] buffer = result.AsyncState as byte[];
            this.DataReceived(this, new DataEventArgs(buffer.Take(read).ToArray()));
            Array.Clear(buffer, 0, buffer.Length);
            this.stream.BeginRead(buffer, 0, buffer.Length, ReadCallback, buffer);
        }
 
        private void WriteCallback(IAsyncResult result)
        {
            this.stream.EndWrite(result);
        }
 
        private NetworkStream stream;
        private TcpClient tcp;
        private bool startConnect;
    }
}

Oraz definicja zdarzeń odebrania danych

Handlers.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.TCP
{
    internal class DataEventArgs : EventArgs
    {
        public DataEventArgs(byte[] data)
        {
            this.Data = data;
            this.Entry = DateTime.Now;
        }
 
        public DateTime Entry { get; private set; }
 
        public byte[] Data { get; set; }
    }
 
    internal class ExceptionEventArgs: EventArgs
    {
        public ExceptionEventArgs(Exception exception)
        {
            this.Exception = exception;
        }
 
        public Exception Exception { get; set; }
    }
    internal delegate void DataEventHandler(object sender, DataEventArgs e);
    internal delegate void ExceptionEventHandler(object sender, ExceptionEventArgs e);
}

Protokół GG

Nasza implementacja klienta będzie działać zgodnie z nowymi wersjami GG, tj. od wersji 8.0 i wzwyż.

Format pakietu

Wszystkie transmitowane dane pomiędzy klientem a serwerem są opakowywane w specjalną strukturę. Każdy pakiet ma następujący format:

  • Rodzaj pakietu: 32 bitowa liczba
  • Długość pakietu: 32 bitowa liczba
  • Pole danych: ciąg bajtów zgodnie z zadeklarowaną długością

Na początek utworzymy klasę bazową pakietu, która posłuży do definicji pozostałych rodzajów pakietów. Utwórz folder Packets w głównym katalogu projektu oraz klasę Packet.

Packet.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
 
namespace MTGG.Packets
{
    internal class Packet
    {
        public Packet()
        {
            this.stream = new MemoryStream();
            this.reader = new BinaryReader(this.stream, ASCIIEncoding.ASCII);
            this.writer = new BinaryWriter(this.stream, ASCIIEncoding.ASCII);
        }
 
        public Packet(PacketType type) : this()
        {
            this.PacketType = type;
        }
 
        public void Finish()
        {
            long len = this.stream.Length - 8;
            this.writer.Seek(4, SeekOrigin.Begin);
            this.writer.Write((uint)len);
        }
 
        public virtual void Write()
        {
            this.stream.SetLength(0);
            this.writer.Write((uint)this.PacketType);
            this.writer.Write((uint)0);
        }
 
        public virtual void Read()
        {
            this.stream.Seek(0, SeekOrigin.Begin);
            this.PacketType = (PacketType)reader.ReadUInt32();
            reader.ReadUInt32();
        }
 
        public override string ToString()
        {
            return String.Format("Packet {0}  {1}", this.PacketType.ToString(), PacketType.ToString("X2"));
        }
 
        public PacketType PacketType
        {
            get;
            protected set;
        }
 
        public byte[] RawData
        {
            get { return this.stream.ToArray(); }
            set { this.stream.Write(value, 0, value.Length); }
        }
        private MemoryStream stream;
        protected BinaryWriter writer;
        protected BinaryReader reader;
    }
}

PacketHandlers.cs

Handler dla zdarzenia pakietu przychodzącego:

internal class PacketEventArgs : EventArgs
{
    public PacketEventArgs(Packet packet)
    {
        this.Packet = packet;
    }
 
    public Packet Packet
    {
        get;
        set;
    }
}
internal delegate void PacketEventHandler(object sender, PacketEventArgs e);

PacketType.cs

Identyfikatory obsługiwanych typów pakietów w bibliotece:

internal enum PacketType : uint
{
    Welcome = 0x0001,
    Login80 = 0x0031,
    Login80_OK = 0x0035,
    Login80_Fail = 0x0045,
    Login_Fail = 0x0009,
    Status = 0x0036,
    ChangeStatus = 0x0038,
    EmptyContactList = 0x0012,
    NotifyFirst = 0x000F,
    NotifyLast = 0x0010,
    NotifyReply80 = 0x0037,
    NotifyAdd = 0x000D,
    NotifyRemove = 0x000E,
    UserData = 0x0044,
    SendMessage = 0x002D,
    SendMessageAck = 0x0005,
    RecvMessage = 0x002E,
    RecvMessageAck = 0x0046,
    Ping = 0x0008,
    Pong = 0x0007,
    Disconnect = 0x000B,
    DisconnectACK = 0x000D,
    TypingNotify = 0x0059,
    RecvOwnMessage = 0x005A,
    SessionsInfo = 0x005B,
    SessionDisconnect = 0x0062,
    PubDirRequest = 0x0014,
    PubDirReply = 0x000E,
    ContactListRequest = 0x0040,
    ContactListReply = 0x0041
}

Nawiązywanie połączenia

Jak wspomniałem wcześniej, połączenie z serwerem GG odbywa się poprzez protokoły TCP/IP. Jednakże, aby uzyskać parametry dostępnego serwera GG tj. adres oraz numer portu, należy wysłać żądanie typu GET na odpowiedni adres. W odpowiedzi powinniśmy uzyskać te parametry i na ich podstawie połączyć się poprzez klasę TCPConnector, którą zdefiniowałem w poprzedniej sekcji.

Utwórz klasę GGConnector w głównej przestrzeni projektu.

GGConnector.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MTGG.TCP;
using MTGG.Packets;
using System.Net;
using System.IO;
 
namespace MTGG
{
    internal class GGConnector
    {
        public GGConnector(uint number)
        {
            this.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4 (.NET CLR 3.5.30729)";
            this.Connector = new TCPConnector();
            this.number = number;
        }
 
        public TCPConnector Connector { get; set; }
        public string UserAgent { get; set; }
 
        public void Open()
        {
            if (!this.Connector.IsOpen && this.GetConnectionParameters())
            {
                this.Connector.Open(this.host.ToString(), this.port);
            }
        }
 
        public void Close()
        {
            this.Connector.Close();
        }
 
        public void WritePacket(Packet packet)
        {
            this.Connector.Write(packet.RawData);
        }
 
        private bool GetConnectionParameters()
        {
            string url = String.Format(ggUrl, this.number);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
            request.UserAgent = this.UserAgent;
 
            using (StreamReader stream = new StreamReader(request.GetResponse().GetResponseStream()))
            {
                string response = stream.ReadToEnd().Trim();
                string[] lines = response.Split(' ');
                string ip = lines[3];
                string port = lines[2].Split(':')[1];
 
                return IPAddress.TryParse(ip, out this.host) && Int32.TryParse(port, out this.port);
            }
        }
 
        private const string ggUrl = "http://appmsg.gadu-gadu.pl/appsvc/appmsg_ver8.asp?fmnumber={0}&version=8.0.0.7669";
        private IPAddress host;
        private int port;
        private uint number;
    }
}

Posiadając parametry serwera GG, będzie można połączyć się z nim poprzez klasę TCPConnector. Po pomyślnym nawiązaniu połączenia, serwer odpowie pakietem Welcome. Zdefiniujmy ten pakiet opierając się o klasę bazową Packet.

„Welcome” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    internal class WelcomePacket : Packet
    {
        public uint Seed { get; private set; }
 
        public override void Read()
        {
            base.Read();
            this.Seed = this.reader.ReadUInt32();
        }
    }
}

Pakiet ten zawiera pole Seed, które będzie potrzebne do wygenerowania skrótu z naszego hasła. Ze względów bezpieczeństwa, w pakiecie logującym jest przesyłany skrót zamiast jawego hasła. W odpowiedzi na ten pakiet, musimy odesłać pakiet logujący zawierający dane naszego konta GG oraz parametry połączenia.

„Login” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using MTGG.Security;
 
namespace MTGG.Packets
{
    internal class LoginPacket : Packet
    {
        public LoginPacket(uint number, string password, uint seed, GGStatus status, Channel channel)
        {
            this.PacketType = PacketType.Login80;
            this.Number = number;
            this.Password = password;
            this.Seed = seed;
            this.Language = "pl";
            this.HashType = HashType.SHA1;
            this.Status = status;
            this.Channels = channel;
            this.Features =
                Features.ReceiveMessage |
                Features.ChangeStatus |
                Features.ChangeStatus2 |
                Features.ExtInfoContact |
                Features.NotifyTalk |
                Features.ConfirmReceive |
                Features.MultiLogon |
                Features.Unknown;
            this.LocalIP = IPAddress.None;
            this.LocalPort = 0;
            this.ExternalIP = IPAddress.None;
            this.ExternalPort = 0;
            this.ImageSize = Byte.MaxValue;
            this.Unknown = 0x64;
            this.Client = "Gadu-Gadu Client build 8.0.0.7669";
            this.Description = String.Empty;
        }
 
        public uint Number { get; set; }
 
        public string Password { get; set; }
 
        public uint Seed { get; set; }
 
        public string Language { get; set; }
 
        public HashType HashType { get; set; }
 
        public GGStatus Status { get; set; }
 
        public Channel Channels { get; set; }
 
        public Features Features { get; set; }
 
        public IPAddress LocalIP { get; set; }
 
        public short LocalPort { get; set; }
 
        public IPAddress ExternalIP { get; set; }
 
        public short ExternalPort { get; set; }
 
        public byte ImageSize { get; set; }
 
        public byte Unknown { get; private set; }
 
        public string Client { get; set; }
 
        public string Description { get; set; }
 
        public override void Write()
        {
            base.Write();
 
            byte[] hash;
            if (this.HashType == HashType.GG32)
            {
                hash = GGHash.GG32(this.Password, this.Seed);
            }
            else
            {
                hash = GGHash.SHA1(this.Password, this.Seed);
            }
 
            byte[] description = UTF8Encoding.UTF8.GetBytes(this.Description);
 
            this.writer.Write(this.Number);
            this.writer.Write(UTF8Encoding.UTF8.GetBytes(this.Language));
            this.writer.Write((byte)this.HashType);
            this.writer.Write(hash);
 
            for (int i = 0; i < 64 - hash.Length; ++i)
            {
                this.writer.Write(Byte.MinValue);
            }
 
            this.writer.Write((uint)this.Status);
            this.writer.Write((uint)this.Channels);
            this.writer.Write((uint)this.Features);
            this.writer.Write(BitConverter.ToUInt32(this.LocalIP.GetAddressBytes(), 0));
            this.writer.Write(this.LocalPort);
            this.writer.Write(BitConverter.ToUInt32(this.ExternalIP.GetAddressBytes(), 0));
            this.writer.Write(this.ExternalPort);
            this.writer.Write(this.ImageSize);
            this.writer.Write(this.Unknown);
            this.writer.Write(this.Client.Length);
            this.writer.Write(UTF8Encoding.UTF8.GetBytes(this.Client));
            this.writer.Write(description.Length);
            this.writer.Write(description);
        }
    }
}

Serwer na pakiet Login może odpowiedzieć na dwa sposoby:

  • Login OK: parametry konta są poprawne
  • Login Fail: odrzucenie połączenia

W przypadku braku odpowiedzi serwera GG, oznacza to że serwer „czeka” jeszcze na dane, czyli najprawdopodobniej nie wysłaliśmy zadeklarkowanej ilości bajtów w polu danych pakietu.

Oba typy pakietów są pakietami pustymi – zawierają jedynie nagłówki, bez pola danych. Tak więc nie trzeba definiować dla nich osobnych definicji. Do ich obsługi wystarczy nam bazowa klasa Packet.

Uzupełnijmy teraz brakujące typy dla pakietu logującego.

GGHash

Klasa do obliczania funkcji skrótu z naszego hasła oraz ziarna przesłanego z pakietem Welcome.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
 
namespace MTGG.Security
{
    public enum HashType : byte
    {
        GG32 = 0x01,
        SHA1 = 0x02
    }
 
    internal static class GGHash
    {
        public static byte[] SHA1(string pass, uint seed)
        {
            byte[] passBin = Encoding.UTF8.GetBytes(pass);
            byte[] seedBin = BitConverter.GetBytes(seed);
            List<Byte> mergedBin = new List<byte>();
            mergedBin.AddRange(passBin);
            mergedBin.AddRange(seedBin);
            SHA1 sha1 = SHA1Managed.Create();
            return sha1.ComputeHash(mergedBin.ToArray());
        }
 
        public static byte[] GG32(string password, uint seed)
        {
            uint x, y, z;
            y = seed;
            int p = 0;
            for (x = 0; p < password.Length; p++)
            {
                x = (x &amp; 0xffffff00) | Convert.ToByte(password[p]);
                y ^= x;
                y += x;
                x <<= 8;
                y ^= x;
                x <<= 8;
                y -= x;
                x <<= 8;
                y ^= x;
 
                z = y &amp; 0x1f;
                y = (uint)((uint)y << (int)z) | (uint)((uint)y >> (int)(32 - z));
            }
            return BitConverter.GetBytes(y);
        }
    }
}

Deklaracja statusów

public enum GGStatus : uint
{
    NotAvailable = 0x0001,
    NotAvailableDescription = 0x0015,
    TalkToMe = 0x0017,
    TalkToMeDescription = 0x0018,
    Available = 0x0002,
    AvailableDescription = 0x0004,
    Busy = 0x0003,
    BusyDescription = 0x0005,
    Disturb = 0x0021,
    DisturbDescription = 0x0022,
    Invisible = 0x0014,
    InvisibleDescription = 0x0016,
    Blocked = 0x0006,
    ImageMask = 0x0100,
    AdaptStatus = 0x0400,
    DescriptionMask = 0x4000,
    FriendsMask = 0x8000
}

Deklaracja obsługiwanych kanałów połączenia

[Flags]
public enum Channel : uint
{
    None = 0x00000000,
    Audio = 0x00000001,
    Video = 0x00000002,
    Mobile = 0x00100000,
    AllowUnknownContacts = 0x00800000
}

Deklaracja funkcjonalności klienta

[Flags]
public enum Features : uint
{
    ChangeStatus = 0x00000001,
    ReceiveMessage = 0x00000002,
    ChangeStatus2 = 0x00000004,
    SupportStatus80 = 0x00000010,
    GraphicStatus = 0x00000020,
    UnknownLogin = 0x00000040,
    Unknown = 0x00000100,
    ExtInfoContact = 0x00000200,
    ConfirmReceive = 0x00000400,
    NotifyTalk = 0x00002000,
    MultiLogon = 0x00004000
}

Do obsługi funkcji wylogowania się z serwera GG lub po prostu zmiany bieżącego statusu, należy zdefiniować pakiet Status. Wylogowanie się klienta polega po prostu na ustawieniu statusu na niedostępny (z opisem lub bez) i zamknięcia połączenia TCP.

„Status” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    internal class StatusPacket : Packet
    {
        public StatusPacket(GGStatus status) : this(status, String.Empty) { }
 
        public StatusPacket(GGStatus status, string description)
        {
            this.Status = status;
            this.Description = description;
            this.Flags = Channel.None;
            this.PacketType = PacketType.ChangeStatus;
        }
 
        public GGStatus Status { get; set; }
 
        public Channel Flags { get; set; }
 
        public string Description { get; set; }
 
        public override void Write()
        {
            base.Write();
            byte[] description = UTF8Encoding.UTF8.GetBytes(this.Description);
            writer.Write((uint)this.Status);
            writer.Write((uint)this.Flags);
            writer.Write(description.Length);
            writer.Write(description);
        }
    }
}

Menedżer pakietów

Mając zdefiniowane podstawowe typy pakietów, odpowiedzialne za ustanowienie połączenia, musimy napisać klasę, która będzie zarządzała wysyłaniem/odbieraniem pakietów. Przetwarzanie pakietów będzie sekwencyjne, oparte na kolejce pakietów, z osobnymi wątkami na przetwarzanie odbieranych oraz wysyłanych pakietów.

PacketManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using MTGG.TCP;
using MTGG.Packets;
using System.Diagnostics;
 
namespace MTGG
{
    internal class PacketManager
    {
        public PacketManager(GGConnector connector)
        {
            this.connector = connector;
            this.connector.Connector.DataReceived += new DataEventHandler(this.connector_DataReceived);
 
            this.outputs = new Queue<Packet>();
            this.inputs = new Queue<Packet>();
            this.bufferData = new List<byte>();
 
            this.packets = new Dictionary<PacketType, Type>();
            this.RegisterPackets();
 
            this.connector.Connector.Connected += new EventHandler(Connector_Connected);
            this.connector.Connector.Disconnected += new EventHandler(Connector_Disconnected);
        }
 
        public event PacketEventHandler PacketReceived;
 
        public void Start()
        {
            this.threadProcess = new Thread(new ThreadStart(this.DispatchPackets));
            this.threadSend = new Thread(new ThreadStart(this.SendPackets));
            this.active = true;
 
            this.threadProcess.Start();
            this.threadSend.Start();
        }
 
        public void Stop()
        {
            this.active = false;
            this.threadProcess.Join();
            this.threadSend.Join();
        }
 
        public void AddPacket(Packet packet)
        {
            packet.Write();
            packet.Finish();
            lock (this.outputs)
            {
                this.outputs.Enqueue(packet);
            }
        }
 
        private void DispatchPackets()
        {
            while (this.active)
            {
                Packet packet = null;
                lock (this.inputs)
                {
                    if (this.inputs.Count != 0)
                    {
                        packet = this.inputs.Dequeue();
                    }
                }
 
                if (packet != null)
                {
                    this.PacketReceived(this, new PacketEventArgs(packet));
 
 
                }
                else
                {
                    Thread.Sleep(10);
                }
            }
        }
 
        private void SendPackets()
        {
            while (this.active)
            {
                Packet packet = null;
                lock (this.outputs)
                {
                    if (this.outputs.Count != 0)
                    {
                        packet = this.outputs.Dequeue();
                    }
                }
 
                if (packet != null)
                {
                    this.connector.WritePacket(packet);
                }
                else
                {
                    Thread.Sleep(10);
                }
            }
        }
 
        private Packet CreatePacket(byte[] data)
        {
            Packet packet = null;
            uint messageCode = BitConverter.ToUInt32(data, 0);
 
            Debug.Assert(Enum.IsDefined(typeof(PacketType), messageCode), String.Format(''Unhandled Enum PacketType: {0}'', messageCode));
            Debug.Assert(this.packets.ContainsKey((PacketType)messageCode), String.Format(''Not implemented packet: {0}'', messageCode));
 
            Type type = this.packets[(PacketType)messageCode];
            packet = Activator.CreateInstance(type) as Packet;
            packet.RawData = data;
            packet.Read();
 
            return packet;
        }
 
        private void ProcessData()
        {
            byte[] data = null;
            lock (this.bufferData)
            {
                if (this.bufferData.Count >= 8)
                {
                    int length = BitConverter.ToInt32(bufferData.ToArray(), 4) + 8;
                    if (this.bufferData.Count >= length)
                    {
                        data = this.bufferData.Take(length).ToArray();
                        this.bufferData.RemoveRange(0, length);
                    }
                }
            }
 
            if (data != null)
            {
                Packet packet = this.CreatePacket(data);
                lock (this.inputs)
                {
                    this.inputs.Enqueue(packet);
                }
            }
        }
 
        private void RegisterPackets()
        {
            this.packets.Add(PacketType.Welcome, typeof(WelcomePacket));
            this.packets.Add(PacketType.Login80, typeof(LoginPacket));
            this.packets.Add(PacketType.Login80_OK, typeof(Packet));
            this.packets.Add(PacketType.Login80_Fail, typeof(Packet));
            this.packets.Add(PacketType.Login_Fail, typeof(Packet));
        }
 
        private void Connector_Disconnected(object sender, EventArgs e)
        {
            this.Stop();
        }
 
        private void Connector_Connected(object sender, EventArgs e)
        {
            this.Start();
        }
 
        private void connector_DataReceived(object sender, DataEventArgs e)
        {
            lock (this.bufferData)
            {
                this.bufferData.AddRange(e.Data);
            }
            this.ProcessData();
        }
 
        private bool active;
        private GGConnector connector;
        private Thread threadProcess;
        private Thread threadSend;
        private Dictionary<PacketType, Type> packets;
        private Queue<Packet> outputs;
        private Queue<Packet> inputs;
        private List<byte> bufferData;
    }
}

Serwer GG przysyła dane niekoniecznie w całości. Jeśli pakiet jest duży, może przysyłać porcjami danych. Należy więc zapewnić duży bufor odbiorczy i deserializować pakiety, dopiero gdy serwer GG skończy przysyłać dane. Można to sprawdzić na podstawie długości pola danych w nagłówku pakietu.

Obsługa kolejnych pakietów jest bardzo prosta. Do słownika packets wprowadzamy identyfikator pakietu oraz klasę odpowiedzialną za jego definicję. W przypadku odebrania z serwera GG danych, w którym nagłówek pakietu będzie miał identyfikator pakietu Welcome, menedżer pakietu utworzy klasę WelcomePacket i wywoła metodę Read aby przetworzyć pakiet z ciągu bajtów, zachowując jego specyfikę.

Klient GG

Teraz napiszemy klasę, która będzie na styku biblioteki z aplikacją, spajając menedżer pakietów oraz komendy wysyłane przez użytkownika. Będzie odpowiedzialna za zarządzanie stanem połączenia, wysyłaniem wiadomości, reagowaniem na komunikaty serwera itd. Sukcesywnie będziemy ją uzupełniać, w miarę postępu rozwoju obsługiwanych funkcjonalności.

GGClient.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.IO;
using MTGG.Packets;
using System.IO.Compression;
 
namespace MTGG
{
    public enum State
    {
        Connected,
        Disconnected
    }
 
    public class GGClient
    {
        public GGClient(uint number, string password) : this(number, password, Channel.None) { }
 
        public GGClient(uint number, string password, Channel options)
        {
            this.number = number;
            this.password = password;
            this.channels = options;
            this.State = State.Disconnected;
 
            this.connector = new GGConnector(number);
            this.packetManager = new PacketManager(this.connector);
            this.packetManager.PacketReceived += new PacketEventHandler(packetManager_PacketReceived);
 
            this.timerPing = new Timer(PingInterval);
            this.timerPing.Elapsed += new ElapsedEventHandler(this.timerPing_Elapsed);
        }
 
        public event EventHandler Connected;
        public event EventHandler Disconnected;
        public event EventHandler LoggedFail;
 
        public State State { get; private set; }
 
        public void LogOn(GGStatus status)
        {
            this.loginStatus = status;
            this.connector.Open();
        }
 
        public void LogOut(string description)
        {
            GGStatus status = String.IsNullOrEmpty(description) ? GGStatus.NotAvailable :
            GGStatus.NotAvailableDescription;
            StatusPacket packet = new StatusPacket(status, description);
            this.packetManager.AddPacket(packet);
            this.State = State.Disconnected;
            this.packetManager.Stop();
            this.connector.Close();
        }
 
        public void ChangeStatus(GGStatus status, string description)
        {
            StatusPacket packet = new StatusPacket(status, description);
            this.packetManager.AddPacket(packet);
        }
 
        private void packetManager_PacketReceived(object sender, PacketEventArgs e)
        {
            switch (e.Packet.PacketType)
            {
                case PacketType.Welcome:
                    WelcomePacket welcome = e.Packet as WelcomePacket;
                    LoginPacket packet = new LoginPacket(this.number, this.password, welcome.Seed,
                        this.loginStatus, this.channels);
                    this.packetManager.AddPacket(packet);
                    break;
 
                case PacketType.Login80_OK:
                    this.State = State.Connected;
                    this.timerPing.Start();
                    if (this.Connected != null)
                    {
                        this.Connected(this, EventArgs.Empty);
                    }
                    break;
 
                case PacketType.Login80_Fail:
                    if (this.LoggedFail != null)
                    {
                        this.LoggedFail(this, EventArgs.Empty);
                    }
                    break;
 
                case PacketType.Disconnect:
                    this.State = State.Disconnected;
                    this.timerPing.Stop();
                    if (this.Disconnected != null)
                    {
                        this.Disconnected(this, EventArgs.Empty);
                    }
                    break;
            }
        }
 
        private void timerPing_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (this.State == State.Connected)
            {
                this.packetManager.AddPacket(new Packet(PacketType.Ping));
            }
        }
 
        private GGStatus loginStatus;
        private Channel channels;
        private const double PingInterval = 240000;
        private PacketManager packetManager;
        private Timer timerPing;
        private GGConnector connector;
        private uint number;
        private string password;
    }
}

Test biblioteki

Posiadając zaimplementowane podstawowe pakiety protokołu GG, czas na przetestowanie poprawności logowania. Utwórz aplikację i dodaj do referencji zbudowaną bibliotekę.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
using MTGG;
 
namespace TestMTGG
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void logOn_Click(object sender, EventArgs e)
        {
            client = new GGClient(1234567890, "TAJNE HASŁO", Channel.Mobile);
            client.Connected += new EventHandler(client_Connected);
            client.Disconnected += new EventHandler(client_Disconnected);
            client.LoggedFail += new EventHandler(client_LoggedFail);
            client.LogOn(GGStatus.Busy);
        }
 
        void client_LoggedFail(object sender, EventArgs e)
        {
            MessageBox.Show("Log fail");
        }
 
        void client_Disconnected(object sender, EventArgs e)
        {
            MessageBox.Show("DisConnected");
        }
 
        void client_Connected(object sender, EventArgs e)
        {
            MessageBox.Show("Connected");
        }
 
        private GGClient client;
    }
}

Podsumowanie

W tej części artykułu, pokazałem jak nawiązać połączenie z serwerem GG oraz zdefiniowałem podstawowe pakiety. Na tę chwilę mamy działający mechanizm obsługi pakietów wychodzących oraz przychodzących. Można się zalogować do serwera GG oraz zmieniać swój status. Zapraszam do następnej części artykułu, gdzie rozszerzymy funkcjonalność biblioteki o obsługę kontaktów.


Podobne artykuły

Komentarze

Jedna odpowiedź do “Bot GG w C# – implementacja protokołu – logowanie”

  1. Cosmo napisał(a):

    Super artukuł. Mocno rozjaśnił światełko w mej głowie:) Wielki szacun. Pozdrawiam

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *