Protokół GG

W poprzedniej części artykułu, zdefiniowałem procedurę logowania do serwera GG oraz pomocnicze klasy, służące do odbierania/wysyłania i przetwarzania pakietów.

Kontakty

Po pomyślnym zalogowaniu się do serwera, należy wysłać listę kontaktów, którą posiadamy w komunikatorze. Jest to czynność niezbędna, aby pobrać ich statusy i powiadomić znajomych, o naszym statusie. Listę kontaktów wysyłamy hurtowo – w jednej paczce maksymalnie 400 kontaktów w pakiecie NotifyPacket. Jeśli nie mamy żadnych kontaktów na liście, to wysyłamy pusty pakiet z identyfikatorem EmptyContactList. Ostatnia paczka z kontaktami, musi mieć specjalny identyfikator w nagłówku pakietu, aby serwer „wiedział”, że zakończyliśmy wysyłanie całej listy kontaktów.

GGContact.cs

Pomocnicza klasa reprezentująca kontakt GG. Zawiera podstawowe informacje takie jak numer, status oraz typ (znajomy, zablokowany), ale także rozszerzone informacje (dostępne od nowego protokołu).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG
{
    public class GGContact
    {
        public GGContact(uint number, ContactType type)
        {
            this.Number = number;
            this.ContactType = type;
        }
 
        public uint Number { get; set; }
 
        public ContactType ContactType { get; set; }
 
        public ClientState State { get; internal set; }
 
        public List<UserDataAttributes> ExtendedInfo { get; internal set; }
    }
 
    [Flags]
    public enum ContactType : byte
    {
        Buddy = 0x01,
        Friend = 0x02,
        Blocked = 0x04
    }
 
    public class ContactEventArgs : EventArgs
    {
        public ContactEventArgs(GGContact contact)
        {
            this.Contact = contact;
        }
 
        public GGContact Contact { get; private set; }
    }
 
    public delegate void ContactEventHandler(object sender, ContactEventArgs e);
}

Oraz klasa definiująca stan kontaktu

ClientState.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
 
namespace MTGG
{
    public class ClientState
    {
        public uint Number { get; set; }
 
        public GGStatus Status { get; set; }
 
        public Features Features { get; set; }
 
        public IPAddress RemoteIP { get; set; }
 
        public ushort RemotePort { get; set; }
 
        public byte ImageSize { get; set; }
 
        public byte Unknown { get; set; }
 
        public Channel Flags { get; set; }
 
        public string Description { get; set; }
    }
}

„UserData” pakiet

Nowa specyfikacja protokołu GG (>= 8.0), umożliwia ustawienie dodatkowych informacji kontaktu. Jeśli podczas logowania, ustawiona zostanie flaga ExtInfoContact w polu Features, będziemy otrzymywać dodatkowe pakiety UserDataPacket.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MTGG.Packets;
 
namespace MTGG
{
    internal class UserDataPacket : Packet
    {
        public UserDataPacket()
        {
            this.userData = new List<UserData>();
        }
 
        public UserData[] Attributes
        {
            get { return this.userData.ToArray(); }
        }
 
 
        public override void Read()
        {
            base.Read();
 
            uint type = this.reader.ReadUInt32();
            uint count = this.reader.ReadUInt32();
 
            for (uint i = 0; i < count; ++i)
            {
                uint number = this.reader.ReadUInt32();
                UserData data = new UserData();
                data.Number = number;
                this.userData.Add(data);
 
                uint count2 = this.reader.ReadUInt32();
 
                for (uint j = 0; j < count2; ++j)
                {
                    int len = this.reader.ReadInt32();
                    string name = UTF8Encoding.UTF8.GetString(this.reader.ReadBytes(len));
                    uint typeAttr = this.reader.ReadUInt32();
                    len = this.reader.ReadInt32();
                    string value = UTF8Encoding.UTF8.GetString(this.reader.ReadBytes(len));
 
                    UserDataAttributes attr = new UserDataAttributes();
                    attr.Name = name;
                    attr.Type = typeAttr;
                    attr.Value = value;
 
                    data.Attributes.Add(attr);
                }
            }
        }
        private List<UserData> userData;
    }
}
 
public class UserData
{
    public UserData()
    {
        this.Attributes = new List<UserDataAttributes>();
    }
 
    public uint Number
    {
        get;
        set;
    }
 
    public List<UserDataAttributes> Attributes
    {
        get;
        private set;
    }
}
 
public class UserDataAttributes
{
    public uint Type
    {
        get;
        set;
    }
 
    public string Name
    {
        get;
        set;
    }
 
    public string Value
    {
        get;
        set;
    }
}

Pozostało nam zdefiniowanie klasy NotifyPacket, która będzie odpowiedzialna za przesłanie porcji listy kontaktów. Serwer na ten pakiet odpowie pakietem NotifyReply, zawierającą informacje o stanie naszych znajomych.

„Notify” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
 
namespace MTGG.Packets
{
    internal class NotifyPacket : Packet
    {
        public NotifyPacket(PacketType type)
        {
            this.Contacts = new List<GGContact>();
            this.PacketType = type;
        }
 
        public List<GGContact> Contacts { get; private set; }
 
        public override void Write()
        {
            base.Write();
            foreach (GGContact contact in this.Contacts)
            {
                this.writer.Write(contact.Number);
                this.writer.Write((byte)contact.ContactType);
            }
        }
    }
}

„NotifyReply” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
 
namespace MTGG.Packets
{
    internal class NotifyReplyPacket : Packet
    {
        public NotifyReplyPacket()
        {
            this.clients = new List<ClientState>();
        }
 
        public ClientState[] ClientStates
        {
            get { return this.clients.ToArray(); }
        }
 
        public override void Read()
        {
            base.Read();
            while (this.reader.BaseStream.Position != this.reader.BaseStream.Length)
            {
                ClientState reply = new ClientState();
                reply.Number = reader.ReadUInt32();
                reply.Status = (GGStatus)reader.ReadUInt32();
                reply.Features = (Features)reader.ReadUInt32();
                reply.RemoteIP = new IPAddress(reader.ReadUInt32());
                reply.RemotePort = reader.ReadUInt16();
                reply.ImageSize = reader.ReadByte();
                reply.Unknown = reader.ReadByte();
                reply.Flags = (Channel)reader.ReadUInt32();
                uint len = reader.ReadUInt32();
                reply.Description = UTF8Encoding.UTF8.GetString(reader.ReadBytes((int)len));
                this.clients.Add(reply);
            }
        }
        private List<ClientState> clients;
    }
}

W jednym pakiecie może być wiele rekordów  o stanie kontaktu. Czytamy więc do końca strumienia.

Menedżer pakietów

Należy teraz zaktualizować klasę PacketManager o obsługę nowych typów pakietów. W tym celu dodaj następujące wpisy w metodzie RegisterPackets.

this.packets.Add(PacketType.Status, typeof(NotifyReplyPacket));
this.packets.Add(PacketType.NotifyReply80, typeof(NotifyReplyPacket));
this.packets.Add(PacketType.UserData, typeof(UserDataPacket));

Mając zdefiniowane wszystkie typy pakietów związanymi ze statusami kontaktów, należy zmodyfikować klasę GGClient do zarządzania kontaktami i udostępnienia odpowiednich zdarzeń.

Klient GG

Dodaj następujące zdarzenia w pliku GGClient.cs. Posłużą one do informowania aplikacj konsumenckieji o zmianie stanu kontaktu.

public event ContactEventHandler ContactStateChanged;
public event ContactEventHandler ContactInfoReceived;

Oraz metody do zarządzania kontaktmi:

private Dictionary<uint, GGContact> contacts;
 
public GGContact[] Contacts
{
    get { return this.contacts.Values.ToArray(); }
}
 
public void AddContact(uint number, ContactType type)
{
    this.AddContact(new GGContact(number, type));
}
 
public void AddContact(GGContact contact)
{
    this.contacts.Add(contact.Number, contact);
    if (this.State == State.Connected)
    {
        NotifyPacket notify = new NotifyPacket(PacketType.NotifyAdd);
        notify.Contacts.Add(contact);
        this.packetManager.AddPacket(notify);
    }
}
 
public void RemoveContact(uint number)
{
    GGContact contact = this.contacts[number];
    this.RemoveContact(contact);
}
 
public void RemoveContact(GGContact contact)
{
    if (this.State == State.Connected)
    {
        NotifyPacket notify = new NotifyPacket(PacketType.NotifyRemove);
        notify.Contacts.Add(contact);
        this.packetManager.AddPacket(notify);
    }
    this.contacts.Remove(contact.Number);
}
 
public void BlockContact(uint number)
{
    GGContact contact = this.contacts[number];
}
 
public void BlockContact(GGContact contact)
{
    NotifyPacket notifyRemove = new NotifyPacket(PacketType.NotifyRemove);
    notifyRemove.Contacts.Add(contact);
    this.packetManager.AddPacket(notifyRemove);
 
    contact.ContactType = ContactType.Blocked;
    NotifyPacket notifyAdd = new NotifyPacket(PacketType.NotifyAdd);
    notifyRemove.Contacts.Add(contact);
    this.packetManager.AddPacket(notifyAdd);
}
 
private void SendContactList()
{
    if (this.contacts.Count == 0)
    {
        this.packetManager.AddPacket(new Packet(PacketType.EmptyContactList));
    }
    else
    {
        int countPackets = (int)Math.Ceiling(this.contacts.Count / 400.0);
        for (int i = 0; i < countPackets; ++i)
        {
            PacketType type = PacketType.NotifyFirst;
            if (i == (countPackets - 1))
            {
                type = PacketType.NotifyLast;
            }
            NotifyPacket packet = new NotifyPacket(type);
            packet.Contacts.AddRange(this.contacts.Values.Skip(i * 400).Take(400));
            this.packetManager.AddPacket(packet);
        }
    }
}

Pozostało nam zaktualizowanie reguł reakcji na pakiety w zdarzeniu odbioru pakietów w zdarzeniu odbioru pakietów:

case PacketType.Login80_OK:
    this.State = State.Connected;
    this.SendContactList();
    this.timerPing.Start();
    if (this.Connected != null)
    {
        this.Connected(this, EventArgs.Empty);
    }
    break;
 
case PacketType.UserData:
    UserDataPacket userData = e.Packet as UserDataPacket;
    foreach (UserData data in userData.Attributes)
    {
        if (this.contacts.ContainsKey(data.Number))
        {
            this.contacts[data.Number].ExtendedInfo = data.Attributes;
            if (this.ContactInfoReceived != null)
            {
                this.ContactInfoReceived(this, new ContactEventArgs(this.contacts[data.Number]));
            }
        }
    }
    break;
 
case PacketType.NotifyReply80:
case PacketType.Status:
    NotifyReplyPacket notify = e.Packet as NotifyReplyPacket;
    foreach (ClientState state in notify.ClientStates)
    {
        this.contacts[state.Number].State = state;
        if (this.ContactStateChanged != null)
        {
            this.ContactStateChanged(this, new ContactEventArgs(this.contacts[state.Number]));
        }
    }
    break;

Test biblioteki

Uzupełnijmy przykład użycia biblioteki o obsługę kontaktów:

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.ContactStateChanged += client_ContactStateChanged;
            client.ContactInfoReceived += client_ContactInfoReceived;
            client.LoggedFail += new EventHandler(client_LoggedFail);
 
            client.AddContact(9876543, ContactType.Buddy);
            client.AddContact(8765432, ContactType.Buddy);
            client.AddContact(7654321, ContactType.Buddy);
            client.LogOn(GGStatus.Busy);
        }
 
        void client_ContactInfoReceived(object sender, ContactEventArgs e)
        {
            foreach (UserDataAttributes attr in e.Contact.ExtendedInfo)
            {
                MessageBox.Show(String.Format("{0} - {1}", attr.Name, attr.Value));
            }
        }
 
        void client_ContactStateChanged(object sender, ContactEventArgs e)
         {
             MessageBox.Show(String.Format("{0} {1}",
                 e.Contact.Number, e.Contact.State.Description));
         }
 
        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

Po dwóch częściach artykułu możemy logować się do serwera GG, zmieniać status, pobierać informacje o znajomych z listy kontaktów. W następnej części artykułu rozszerzymy możliwości biblioteki o pisanie wiadomości.


Podobne artykuły

Komentarze

Dodaj komentarz

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