Protokół GG

Przeszukiwanie katalogu publicznego polega na zdefiniowaniu odpowiedniego pakietu wraz z tzw. numerem startowym. Numer ten określa od jakiego miejsca, serwer ma zwrócić kolejną porcję danych (przycisk Więcej w aplikacji GG).

Katalog publiczny

W katalogu publicznym możemy szukać znajomych po określonych kryteriach takich jak płeć, miejscowość, status itd. Do przeszukiwania katalogu publicznego służy pakiet PubDir. Za pomocą tego pakietu, możemy odczytać własne dane z katalogu oraz je zaktualizować. Ten rodzaj pakietu jest taki sam dla zgłoszenia żądania dostępu do katalogu jak i dla odpowiedzi z serwera GG. Z tą różnicą, że pakiet zwrotny może zawierać wiele rekordów pasujących do kryteriów wyszukiwania. Wszystkie parametry wyszukiwania są kodowane w formacie: parameter wartość oddzielone znakiem .

„PubDir” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    public enum PubDirMode : byte
    {
        Write = 0x01,
        Read = 0x02,
        Search = 0x03,
        Reply = 0x05
    }
 
    internal class PubDirPacket : Packet
    {
        public PubDirPacket() { }
        public PubDirPacket(PubDirMode mode, DateTime seq, string query)
        {
            this.PacketType = PacketType.PubDirRequest;
            this.PubDirMode = mode;
            this.Sequence = UTC.DateToUnixTimestamp(seq);
            this.Query = query;
        }
 
        public PubDirMode PubDirMode { get; set; }
 
        public uint Sequence { get; set; }
 
        public string Query { get; set; }
 
        public override void Write()
        {
            base.Write();
            this.writer.Write((byte)this.PubDirMode);
            this.writer.Write(this.Sequence);
            this.writer.Write(Encoding.GetEncoding(1250).GetBytes(this.Query));
        }
 
 
        public override void Read()
        {
            base.Read();
            this.PubDirMode = (PubDirMode)this.reader.ReadByte();
            this.Sequence = this.reader.ReadUInt32();
            this.Query = Encoding.GetEncoding(1250).GetString(this.reader.ReadBytes(short.MaxValue));
        }
 
    }
}

Oraz klasa PubDirData z danymi katalogu publicznego i metody pomocnicze do konwersji pomiędzy tą klasą a tekstowym zapisaem kryteriów wyszukiwania.

PubDirData.cs

Zdefiniujemy klasę ze wszystkimi możliwymi polami katalogu publicznego. Dodatkowo napiszemy własny atrybut Parameter, który zmapuje pola klasy z odpowiednią nazwą pola w zapytaniu dla katalogu publicznego. Na jego podstawie, utworzone zostanie odpowiednie zapytanie tekstowe (pole Query w pakiecie).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
 
namespace MTGG
{
    public enum Gender : byte
    {
        Female = 0x01,
        Male = 0x02
    }
 
    public class PubDirData
    {
        [Parameter("FmNumber")]
        public uint? Number { get; set; }
 
        [Parameter("FmStatus")]
        public GGStatus? Status { get; set; }
 
        [Parameter("firstname")]
        public string FirstName { get; set; }
        [Parameter("lastname")]
        public string LastName { get; set; }
 
        [Parameter("nickname")]
        public string NickName { get; set; }
 
        [Parameter("birthyear")]
        public ushort? Birth { get; set; }
 
        [Parameter("city")]
        public string City { get; set; }
 
        [Parameter("gender")]
        public Gender? Gender { get; set; }
 
        [Parameter("ActiveOnly")]
        public bool? ActiveUsers { get; set; }
 
        [Parameter("familyname")]
        public string FamilyName { get; set; }
 
        [Parameter("familycity")]
        public string FamilyCity { get; set; }
 
        [Parameter("fmstart")]
        public uint? StartNumber { get; set; }
 
        [Parameter("nextstart")]
        public uint? NextNumber { get; set; }
    }
 
    internal class ParameterAttribute : Attribute
    {
        public ParameterAttribute(string name)
        {
            this.Parameter = name;
        }
 
        public string Parameter { get; set; }
    }
 
    public class PubDirEventArgs : EventArgs
    {
        public PubDirEventArgs(PubDirData[] data)
        {
            this.PubDirData = data;
        }
 
        public PubDirData[] PubDirData { get; set; }
    }
 
    public delegate void PubDirEventHandler(object sender, PubDirEventArgs e);
}

Oraz klasa konwertująca pola klas PubDirData na odpowiednie zapytanie (oraz na odwrót).

internal class PubDirConverter
{
    public static PubDirData[] ToPubDirData(string query)
    {
        List<PubDirData> pubDirs = new List<PubDirData>();
        string[] contacts = query.Split(new string[] { "" }, StringSplitOptions.RemoveEmptyEntries);
        PropertyInfo[] properties = typeof(PubDirData).GetProperties();
 
        foreach (string contact in contacts)
        {
            PubDirData data = new PubDirData();
            pubDirs.Add(data);
            string[] param = contact.Split('').Where(ff => ff != "").ToArray();
 
            for (int i = 0; i < param.Count(); i += 2)
            {
                PropertyInfo p = properties.Single(f => ((ParameterAttribute)(f.GetCustomAttributes(typeof(ParameterAttribute), false).First())).Parameter == param[i]);
                Type type = p.PropertyType;
 
                if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    type = type.GetGenericArguments().First();
                }
 
                object val;
                if (type == typeof(string))
                {
                    val = param[i + 1];
                }
                else if (type.IsEnum)
                {
                    val = Enum.Parse(type, param[i + 1]);
                }
                else
                {
                    val = Convert.ChangeType(Convert.ToUInt32(param[i + 1]), type);
                }
                p.SetValue(data, val, null);
            }
        }
        return pubDirs.ToArray();
    }
 
    public static string ToQueryString(PubDirData data)
    {
        if (data == null)
        {
            return String.Empty;
        }
 
        StringBuilder query = new StringBuilder();
        IEnumerable<PropertyInfo> properties = typeof(PubDirData).GetProperties().Where(f => f.GetValue(data, null) != null);
        foreach (PropertyInfo p in properties)
        {
            string param = ((ParameterAttribute)p.GetCustomAttributes(typeof(ParameterAttribute), false).First()).Parameter;
            object value = p.GetValue(data, null);
            string val = value.GetType() == typeof(string) ? (string)value : Convert.ToUInt32(value).ToString();
            query.Append(String.Format("{0}{1}", param, val));
        }
        return query.ToString();
    }
}

Menedżer pakietów

Standardowo, zarejestrujmy nowy pakiet w klasie PacketManager w metodzie RegisterPackets.

PacketManager.cs

this.packets.Add(PacketType.PubDirReply, typeof(PubDirPacket));

Klient GG

Wyszukiwanie w katalogu publicznym jest asynchroniczne. Po wysłaniu pakietu PubDirPacket zawierający kryteria wyszukiwania, serwer po czasie odpowie tym samym pakietem. Dlatego w klasie GGClient udostępnimy zdarzenie PubDirReceived, dzięki któremu w aplikacji będzie można odświeżyć rezultaty wyszukiwania.

GGClient.cs

public event PubDirEventHandler PubDirReceived;
 
public void AsyncPubDirRequest(PubDirMode mode, PubDirData request)
{
    PubDirPacket packet = new PubDirPacket(mode, DateTime.Now, PubDirConverter.ToQueryString(request));
    this.packetManager.AddPacket(packet);
}

Oraz uzupełnienie reguł przetwarzania pakietów w zdarzeniu odbioru pakietów.

case PacketType.PubDirReply:
    PubDirPacket dirPacket = e.Packet as PubDirPacket;
    PubDirData[] pubData = PubDirConverter.ToPubDirData(dirPacket.Query);
    if (this.PubDirReceived != null)
    {
        this.PubDirReceived(this, new PubDirEventArgs(pubData));
    }
    break;

Test biblioteki

Przetestujmy działanie biblioteki o wyszukiwanie kontaktów w katalogu publicznym. Uzupełnimy dotychczasowy przykład użycia biblioteki o wyszukiwanie w katalogu publicznym. W tym przykładzie wyszukamy wszystkich Janów, mieszkających w Poznaniu.

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.ContactTyping += new TypingEventHandler(client_ContactTyping);
            client.MessageReceived += client_MessageReceived;
            client.MessageStateChanged += client_MessageStateChanged;
            client.PubDirReceived += client_PubDirReceived;
 
            client.AddContact(9876543, ContactType.Buddy);
            client.AddContact(8765432, ContactType.Buddy);
            client.AddContact(7654321, ContactType.Buddy);
            client.LogOn(GGStatus.Busy);
        }
 
        private void searchPubDir_Click(object sender, EventArgs e)
        {
            PubDirData pubDir = new PubDirData();
            pubDir.FirstName = "Jan";
            pubDir.City = "Poznań";
            pubDir.Gender = Gender.Male;
            client.AsyncPubDirRequest(MTGG.Packets.PubDirMode.Search, pubDir);
        }
 
        void client_PubDirReceived(object sender, PubDirEventArgs e)
        {
            foreach(PubDirData data in e.PubDirData)
            {
                MessageBox.Show(data.FirstName);
            }
        }
 
        void client_MessageStateChanged(object sender, MessageEventArgs e)
        {
            if (e.Status == MTGG.Packets.MessageStatus.Delivered)
            {
                MessageBox.Show(e.Message.PlainMessage);
            }
        }
 
        void client_MessageReceived(object sender, MessageEventArgs e)
        {
            MessageBox.Show(e.Message.PlainMessage);
        }
 
        void client_ContactTyping(object sender, TypingEventArgs e)
        {
            MessageBox.Show(String.Format("{0}  [{1}]", e.Contact.Number, e.Value));
        }
 
        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");
            GGContact contact = this.client.Contacts.First();
            GGMessage msg = new GGMessage(contact);
            msg.PlainMessage = "Hello";
            this.client.NotifyTyping(contact, (ushort)msg.PlainMessage.Length);
            this.client.SendMessage(msg);
        }
 
        private GGClient client;
    }
}

Podsumowanie

To już ostatnia część artykułu dotyczącej implementacji protokołu GG. W tym wpisie pokazałem jak wykorzystać funkcjonalność katalogu publicznego. Na stan obecny, mamy zaimplementowaną prawie całą podstawową funkcjonalność klienta GG. Brakuje jeszcze obsługi mechanizmu sesji (mało istotne moim zdaniem) oraz importu/eksportu kontaktów z serwera GG (to już ciekawsze). Jeśli będzie taka potrzeba to uzupełnię cykl o te aspekty.


Podobne artykuły

Komentarze

Dodaj komentarz

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