Protokół GG

Wysyłanie/odbieranie wiadomości to bardziej złożony temat niż w przypadku implementacji wcześniejszych funkcjonalności. Dochodzi kwestia kompatybilności ze starymi klientami. Idealnie by było, żeby operować jednym formatem w aplikacji konsumenckiej np. w HTML, biblioteka natomiast konwertowała by to na zapis kompatybilny ze starą wersją protokołu. W implementacji pakietów dot. wiadomości, pominę kwestię przesyłania obrazków.

Kodowanie czasu

Niektóre pakiety przesyłają znacznik czasu, który jest kodowany jako liczba sekund od 1 Stycznia 1970 roku (czas uniksowy). Poniższa klasa będzie przydatna do kodowania/dekodowania czasu pomiędzy tym formatem a tradycyjnym typem DateTime.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG
{
    internal class UTC
    {
        private static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0,
            DateTimeKind.Utc);
 
        public static DateTime UnixTimestampToDate(uint seconds)
        {
            return UTC.UnixEpoch.AddSeconds(seconds).ToLocalTime();
        }
 
        public static uint DateToUnixTimestamp(DateTime date)
        {
            return (uint)date.Subtract(UTC.UnixEpoch).TotalSeconds;
        }
    }
}

Wysyłanie wiadomości

Od nowego protokołu GG (>= 8.0) wiadomość kodowana jest w formacie HTML w UTF-8. W celu utrzymania kompatybilności ze starymi klientami, wiadomość przesyłana jest także w czystym tekście (kodowanie CP1250) wraz z listą atrybutów RTF.

“SendMessagePacket” pakiet

Pakiet z wysyłaną wiadomością. W tej klasie pakietu, brakuje implementacji konwersji pomiędzy HTML <-> RTF + Plain. Aby biblioteka była kompatybilna ze wszystkimi rodzajami klientów, należy wysyłać wiadomość z polem HTML oraz Plain + lista atrybutów RTF. Na chwilę obecną po prostu musimy się zadowolić wiadomościami wysyłanych czystym tekstem do wszystkich rodzajów klientów, albo sformatowanym HTMLem do nowszych klientów.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
 
namespace MTGG.Packets
{
    internal class SendMessagePacket : Packet
    {
        public SendMessagePacket(uint number, uint[] recipients, string html, string plain, uint sequence)
        {
            this.PacketType = PacketType.SendMessage;
            this.Class = ClassMessage.Chat;
            this.Recipient = number;
            this.sequence = sequence;
            this.recipients = recipients;
            this.attributes = new List<RichTextFormat>();
            this.HtmlMessage = html;
            this.PlainMessage = plain;
        }
 
        public uint Recipient { get; set; }
 
        public ClassMessage Class { get; set; }
 
        public string HtmlMessage { get; set; }
 
        public string PlainMessage { get; set; }
 
        public List<RichTextFormat> Attributes
        {
            get { return this.attributes; }
        }
 
        public override void Write()
        {
            byte[] msgHtml = UTF8Encoding.UTF8.GetBytes(this.HtmlMessage);
            byte[] msgPlain = Encoding.GetEncoding(1250).GetBytes(this.PlainMessage);
            int offsetConference = 0;
            if (this.recipients != null &amp;&amp; this.recipients.Count() != 0)
            {
                offsetConference = 8 + 4 * this.recipients.Count();
            }
 
            base.Write();
            writer.Write(this.Recipient);
            writer.Write(this.sequence);
            writer.Write((uint)this.Class);
 
            writer.Write(msgHtml.Length + 20 + 1);
            writer.Write(msgHtml.Length + msgPlain.Length + 20 + 2 + offsetConference);
 
            writer.Write(msgHtml);
            writer.Write('');
            writer.Write(msgPlain);
            writer.Write('');
 
            if (offsetConference != 0)
            {
                this.writer.Write((byte)0x01);
                this.writer.Write((uint)this.recipients.Count());
                foreach (uint number in this.recipients)
                {
                    this.writer.Write(number);
                }
            }
 
            foreach (RichTextFormat attr in this.Attributes)
            {
                writer.Write(attr.Position);
                writer.Write((byte)attr.FormatType);
                if (attr.Color != null)
                {
                    writer.Write(attr.Color.R);
                    writer.Write(attr.Color.G);
                    writer.Write(attr.Color.B);
                }
            }
        }
 
        private uint[] recipients;
        private List<RichTextFormat> attributes;
        private uint sequence;
    }
}

ClassMessage.cs

Typy wiadomości – wiadomość w okienku, skolejkowana itd.

[Flags]
internal enum ClassMessage : uint
{
    Queued = 0x0001,
    Msg = 0x0004,
    Chat = 0x0008,
    CTCP = 0x0010,
    ACK = 0x0020
}

Oraz klasa atrybutów RTF.

RTF.cs

internal class RichTextFormat
{
    public RichTextFormat(ushort position, FormatType type)
    {
        this.Position = position;
        this.FormatType = type;
    }
 
    public ushort Position { get; set; }
 
    public FormatType FormatType { get; set; }
 
    public Color Color { get; set; }
 
    public RichTextImage Image { get; set; }
}
 
internal class RichTextImage
{
    public uint Size { get; set; }
 
    public uint CRC32 { get; set; }
}

SendMessageAck pakiet

Po wysłaniu wiadomości do adresata, powinniśmy otrzymać pakiet od serwera ze statusem wysłanej wiadomości np. wiadomość nie dostarczona z powodu niedostępności kontaktu.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    internal class SendMessageAckPacket : Packet
    {
        public MessageStatus Status { get; private set; }
 
        public uint Recipient { get; private set; }
 
        public uint Sequence { get; private set; }
 
        public override void Read()
        {
            base.Read();
            this.Status = (MessageStatus)this.reader.ReadUInt32();
            this.Recipient = this.reader.ReadUInt32();
            this.Sequence = this.reader.ReadUInt32();
        }
    }
 
    public enum MessageStatus : uint
    {
        Blocked = 0x01,
        Delivered = 0x02,
        Queued = 0x03,
        MBoxFull = 0x04,
        NotDelivered = 0x06
    }
}

Odbieranie wiadomości

“ReceiveMessage” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;
 
namespace MTGG.Packets
{
    internal class ReceiveMessagePacket : Packet
    {
        public ReceiveMessagePacket()
        {
            this.attributes = new List<RichTextFormat>();
            this.recipients = new List<uint>();
        }
 
        public uint Sender { get; private set; }
 
        public uint[] Recipients
        {
            get { return this.recipients.ToArray(); }
        }
 
        public uint Sequence { get; private set; }
 
        public DateTime Time { get; private set; }
 
        public ClassMessage Class { get; private set; }
 
        public string HtmlMessage { get; private set; }
 
        public string PlainMessage { get; private set; }
 
        public RichTextFormat[] Attributes
        {
            get { return this.attributes.ToArray(); }
        }
 
        public override void Read()
        {
            base.Read();
            this.Sender = reader.ReadUInt32();
            this.Sequence = reader.ReadUInt32();
            uint time = reader.ReadUInt32();
            this.Time = UTC.UnixTimestampToDate(time);
            this.Class = (ClassMessage)reader.ReadUInt32();
            uint offsetPlain = reader.ReadUInt32();
            uint offsetAttributes = reader.ReadUInt32();
 
            long currentPos = this.reader.BaseStream.Position - 8;
 
            List<byte> message = new List<byte>();
            byte tmp;
 
            if (currentPos != offsetPlain)
            {
                while ((tmp = reader.ReadByte()) != 0)
                {
                    message.Add(tmp);
                }
                this.HtmlMessage = UTF8Encoding.UTF8.GetString(message.ToArray());
            }
 
            message.Clear();
            while ((tmp = reader.ReadByte()) != 0)
            {
                message.Add(tmp);
            }
            this.PlainMessage = Encoding.GetEncoding(1250).GetString(message.ToArray());
 
            message.Clear();
 
            while (reader.PeekChar() != -1)
            {
                byte attributeType = this.reader.ReadByte(); //0x02 -> attr
 
                if (attributeType == 0x01)
                {
                    uint count = this.reader.ReadUInt32();
                    for (uint i = 0; i < count; ++i)
                    {
                        this.recipients.Add(this.reader.ReadUInt32());
                    }
                }
                else if(attributeType == 0x02)
                {
                    ushort length = this.reader.ReadUInt16(); // length
                    long range = this.reader.BaseStream.Position + length;
                    while (this.reader.BaseStream.Position < range)
                    {
                        ushort position = reader.ReadUInt16();
                        FormatType type = (FormatType)reader.ReadByte();
                        RichTextFormat block = new RichTextFormat(position, type);
 
                        if (type.HasFlag(FormatType.FontColor))
                        {
                            block.Color = Color.FromArgb(reader.ReadByte(),
                                reader.ReadByte(), reader.ReadByte());
                        }
 
                        if (type.HasFlag(FormatType.Image))
                        {
                            RichTextImage image = new RichTextImage();
                            image.Size = this.reader.ReadUInt32();
                            image.CRC32 = this.reader.ReadUInt32();
                            block.Image = image;
                        }
                        this.attributes.Add(block);
                    }
                }
            }
            this.HtmlMessage = RTF.PlainToHtml(this.PlainMessage, this.Attributes);
        }
        private List<RichTextFormat> attributes;
        private List<uint> recipients;
    }
}

“ReceiveMessageAck” pakiet

Po odebraniu wiadomości należy poinformować serwer o pomyślnym odebraniu pakietu z wiadomością.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    internal class ReceiveMessageAckPacket : Packet
    {
        public ReceiveMessageAckPacket(uint sequence)
        {
            this.PacketType = PacketType.RecvMessageAck;
            this.Sequence = sequence;
        }
 
        public uint Sequence { get; set; }
 
        public override void Write()
        {
            base.Write();
            this.writer.Write(this.Sequence);
        }
    }
}

Powiadamianie o pisaniu

W trakcie pisania w okienku wiadomości, możemy poinformować rozmówcę, ile znaków już napisaliśmy. W tym celu należy wysłać pakiet TypingNotify, zawierający ilość znaków, których jeszcze nie wysłaliśmy. Działa on również w odwrotną stronę – gdy ktoś do nas coś pisze, również otrzymamy pakiet o ilości znaków w jego okienku rozmowy.

“TypingNotify” pakiet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG.Packets
{
    internal class TypingNotifyPacket : Packet
    {
        public TypingNotifyPacket()
        {
            this.PacketType = PacketType.TypingNotify;
        }
 
        public TypingNotifyPacket(uint number, ushort length) : this()
        {
            this.Value = length;
            this.Number = number;
        }
 
        public ushort Value { get; set; }
 
        public uint Number { get; set; }
 
        public override void Read()
        {
            base.Read();
            this.Value = this.reader.ReadUInt16();
            this.Number = this.reader.ReadUInt32();
        }
 
        public override void Write()
        {
            base.Write();
            this.writer.Write(this.Value);
            this.writer.Write(this.Number);
        }
    }
}

TypingHandler.cs

Zdefiniujemy handler do zdarzenia, który będzie powiadamiać aplikację o postępie pisania przez rozmówcę.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG
{
    public class TypingEventArgs : EventArgs
    {
        public TypingEventArgs(ushort value, GGContact contact)
        {
            this.Value = value;
            this.Contact = contact;
        }
 
        public ushort Value { get; internal set; }
 
        public GGContact Contact { get; internal set; }
    }
 
    public delegate void TypingEventHandler(object sender, TypingEventArgs e);
}

Menedżer pakietów

Po zdefiniowaniu pakietów związanymi z odbieraniem wiadomości, należy zarejestrować je w menedżerze pakietów w metodzie RegisterPackets.

this.packets.Add(PacketType.RecvMessage, typeof(ReceiveMessagePacket));
this.packets.Add(PacketType.SendMessageAck, typeof(SendMessageAckPacket));
this.packets.Add(PacketType.TypingNotify, typeof(TypingNotifyPacket));
this.packets.Add(PacketType.RecvOwnMessage, typeof(ReceiveMessagePacket));

Klient GG

Teraz uzupełnimy klasę GGClient o obsługę wiadomości. Zdefiniujmy klasę, która będzie reprezentować wiadomość będącą na styku biblioteki z aplikacją.

GGMessage.cs

using MTGG.Packets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MTGG
{
    public class GGMessage
    {
        public GGMessage(GGContact recipient) : this(new GGContact[] { recipient }) { }
        public GGMessage(uint recipient) : this(new uint[] { recipient }) { }
 
        public GGMessage(GGContact[] recipients) :
            this(recipients.Select(x => x.Number).ToArray()) { }
 
        public GGMessage(uint[] recipients)
        {
            this.Recipients = recipients;
            this.Time = DateTime.Now;
            this.Sequence = UTC.DateToUnixTimestamp(this.Time);
            this.PlainMessage = String.Empty;
            this.HtmlMessage = String.Empty;
        }
 
        public uint Sender { get; internal set; }
 
        public uint[] Recipients { get; private set; }
 
        public DateTime Time { get; internal set; }
 
        public uint Sequence { get; internal set; }
 
        public string HtmlMessage { get; set; }
 
        public string PlainMessage { get; set; }
    }
 
    public class MessageEventArgs : EventArgs
    {
        public MessageEventArgs(MessageStatus status, GGMessage message)
        {
            this.Status = status;
            this.Message = message;
        }
 
        public MessageStatus Status { get; internal set; }
 
        public GGMessage Message { get; internal set; }
    }
    public delegate void MessageEventHandler(object sender, MessageEventArgs e);
}

oraz dodanie metod w klasie GGClient.

GGClient.cs

public event MessageEventHandler MessageReceived;
public event MessageEventHandler MessageStateChanged;
public event TypingEventHandler ContactTyping;
 
public void SendMessage(GGMessage message)
{
    message.Sender = this.number;
    foreach (uint recipient in message.Recipients)
    {
        List<uint> recipients = message.Recipients.ToList();
        recipients.Remove(recipient);
        SendMessagePacket packet = new SendMessagePacket(recipient, recipients.ToArray(),
            message.HtmlMessage, message.PlainMessage, message.Sequence);
        lock (this.messages)
        {
            this.messages.Add(message.Sequence, message);
        }
        this.packetManager.AddPacket(packet);
    }
}
 
public void NotifyTyping(GGContact contact, ushort count)
{
    this.NotifyTyping(contact.Number, count);
}
 
public void NotifyTyping(uint number, ushort count)
{
    TypingNotifyPacket packet = new TypingNotifyPacket(number, count);
    this.packetManager.AddPacket(packet);
}
 
private Dictionary<uint, GGMessage> messages;

case PacketType.TypingNotify:
    TypingNotifyPacket typing = e.Packet as TypingNotifyPacket;
    if (this.ContactTyping != null)
    {
        if (this.contacts.ContainsKey(typing.Number))
        {
            GGContact contact = this.contacts[typing.Number];
            this.ContactTyping(this, new TypingEventArgs(typing.Value, contact));
        }
    }
    break;
 
case PacketType.SendMessageAck:
    SendMessageAckPacket ack = e.Packet as SendMessageAckPacket;
    if (this.messages.ContainsKey(ack.Sequence))
    {
        GGMessage message = this.messages[ack.Sequence];
        lock (this.messages)
        {
            this.messages.Remove(ack.Sequence);
        }
        if (this.MessageStateChanged != null)
        {
            this.MessageStateChanged(this, new MessageEventArgs(ack.Status, message));
        }
    }
    break;
 
case PacketType.RecvMessage:
    ReceiveMessagePacket receive = e.Packet as ReceiveMessagePacket;
    GGMessage msg = new GGMessage(receive.Recipients);
    msg.Sender = receive.Sender;
    msg.HtmlMessage = receive.HtmlMessage;
    msg.PlainMessage = receive.PlainMessage;
    msg.Time = receive.Time;
    msg.Sequence = receive.Sequence;
 
    ReceiveMessageAckPacket confirm = new ReceiveMessageAckPacket(receive.Sequence);
    this.packetManager.AddPacket(confirm);
 
    if (this.MessageReceived != null)
    {
        MessageEventArgs args = new MessageEventArgs(MessageStatus.Delivered, msg);
        this.MessageReceived(this, args);
    }
    break;

Test biblioteki

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.AddContact(9876543, ContactType.Buddy);
            client.AddContact(8765432, ContactType.Buddy);
            client.AddContact(7654321, ContactType.Buddy);
            client.LogOn(GGStatus.Busy);
        }
 
        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

Po tej części artykułu mamy zaimplementowaną całą podstawową funkcjonalność klienta GG. Możemy łączyć się z serwerem GG, zmieniać swój status i obserwować statusy znajomych oraz wysyłać i odbierać wiadomości. W czwartej części artykułu, dodamy możliwość przeglądania katalogu publicznego.


Podobne artykuły

Komentarze

Dodaj komentarz

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