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