Bot GG w C# – implementacja protokołu – wiadomości
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 && 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.
Strona Internetowa
Potrzebujesz ładnej strony internetowej? Zobacz demo na: tej stronie
Komentarze