mirror of
https://github.com/sochix/TLSharp.git
synced 2025-12-06 08:02:00 +01:00
Merge 2d540787e7 into dab442f45b
This commit is contained in:
commit
9ded8c75c2
214
README.md
214
README.md
|
|
@ -151,6 +151,220 @@ To download file you should call **GetFile** method
|
||||||
|
|
||||||
Full code you can see at [DownloadFileFromContactTest](https://github.com/sochix/TLSharp/blob/master/TLSharp.Tests/TLSharpTests.cs#L167)
|
Full code you can see at [DownloadFileFromContactTest](https://github.com/sochix/TLSharp/blob/master/TLSharp.Tests/TLSharpTests.cs#L167)
|
||||||
|
|
||||||
|
# Events Sample code
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TeleSharp.TL;
|
||||||
|
using TLSharp.Core;
|
||||||
|
using System.Linq;
|
||||||
|
using TeleSharp.TL.Messages;
|
||||||
|
|
||||||
|
namespace TLSharpPOC
|
||||||
|
{
|
||||||
|
class MainClass
|
||||||
|
{
|
||||||
|
const int APIId = 0;
|
||||||
|
const string APIHash = "???";
|
||||||
|
const string phone = "???";
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
new MainClass().MainAsync(args).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MainAsync(string[] args)
|
||||||
|
{
|
||||||
|
TelegramClient client = null;
|
||||||
|
|
||||||
|
client = new TelegramClient(APIId, APIHash);
|
||||||
|
// subscribe an event to receive live messages
|
||||||
|
client.Updates += ClientUpdates;
|
||||||
|
await client.ConnectAsync();
|
||||||
|
Console.WriteLine($"Authorised: {client.IsUserAuthorized()}");
|
||||||
|
TLUser user = null;
|
||||||
|
// -- If the user has already authenticated, this step will prevent account from being blocked as it
|
||||||
|
// -- reuses the data from last authorisation.
|
||||||
|
if (client.IsUserAuthorized())
|
||||||
|
user = client.Session.TLUser;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var registered = await client.IsPhoneRegisteredAsync(phone);
|
||||||
|
var hash = await client.SendCodeRequestAsync(phone);
|
||||||
|
Console.Write("Code: ");
|
||||||
|
var code = Console.ReadLine();
|
||||||
|
if (!registered)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Sign up {phone}");
|
||||||
|
user = await client.SignUpAsync(phone, hash, code, "First", "Last");
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Sign in {phone}");
|
||||||
|
user = await client.MakeAuthAsync(phone, hash, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contacts = await client.GetContactsAsync();
|
||||||
|
Console.WriteLine("Contacts:");
|
||||||
|
foreach (var contact in contacts.Users.OfType<TLUser>())
|
||||||
|
{
|
||||||
|
var contactUser = contact as TLUser;
|
||||||
|
Console.WriteLine($"\t{contact.Id} {contact.Phone} {contact.FirstName} {contact.LastName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var dialogs = (TLDialogs) await client.GetUserDialogsAsync();
|
||||||
|
Console.WriteLine("Channels: ");
|
||||||
|
foreach (var channelObj in dialogs.Chats.OfType<TLChannel>())
|
||||||
|
{
|
||||||
|
var channel = channelObj as TLChannel;
|
||||||
|
Console.WriteLine($"\tChat: {channel.Title}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Groups:");
|
||||||
|
TLChat chat = null;
|
||||||
|
foreach (var chatObj in dialogs.Chats.OfType<TLChat>())
|
||||||
|
{
|
||||||
|
chat = chatObj as TLChat;
|
||||||
|
Console.WriteLine($"Chat name: {chat.Title}");
|
||||||
|
var request = new TLRequestGetFullChat() { ChatId = chat.Id };
|
||||||
|
var fullChat = await client.SendRequestAsync<TeleSharp.TL.Messages.TLChatFull>(request);
|
||||||
|
|
||||||
|
var participants = (fullChat.FullChat as TeleSharp.TL.TLChatFull).Participants as TLChatParticipants;
|
||||||
|
foreach (var p in participants.Participants)
|
||||||
|
{
|
||||||
|
if (p is TLChatParticipant chatParticipant)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t{chatParticipant.UserId}");
|
||||||
|
}
|
||||||
|
else if (p is TLChatParticipantAdmin chatParticipantAdmin)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t{chatParticipantAdmin.UserId}**");
|
||||||
|
}
|
||||||
|
else if (p is TLChatParticipantCreator chatParticipantCreator)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t{chatParticipantCreator.UserId}**");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer = new TLInputPeerChat() { ChatId = chat.Id };
|
||||||
|
var msg = await client.GetHistoryAsync(peer, 0, 0, 0);
|
||||||
|
Console.WriteLine(msg);
|
||||||
|
if (msg is TLMessages messages)
|
||||||
|
{
|
||||||
|
foreach (var message in messages.Messages)
|
||||||
|
{
|
||||||
|
if (message is TLMessage m1)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t\t{m1.Id} {m1.Message}");
|
||||||
|
}
|
||||||
|
else if (message is TLMessageService msgService)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t\t{msgService.Id} {msgService.Action}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg is TLMessagesSlice messagesSlice)
|
||||||
|
{
|
||||||
|
bool done = false;
|
||||||
|
int total = 0;
|
||||||
|
while (!done)
|
||||||
|
{
|
||||||
|
foreach (var m1 in messagesSlice.Messages)
|
||||||
|
{
|
||||||
|
if (m1 is TLMessage message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t\t{message.Id} {message.Message}");
|
||||||
|
++total;
|
||||||
|
}
|
||||||
|
else if (m1 is TLMessageService messageService)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t\t{messageService.Id} {messageService.Action}");
|
||||||
|
++total;
|
||||||
|
done = messageService.Action is TLMessageActionChatCreate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg = await client.GetHistoryAsync(peer, total, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Wait in a loop to handle incoming updates. No need to poll.
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
await client.WaitEventAsync(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClientUpdates(TelegramClient client, TLAbsUpdates updates)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Got update: {updates}");
|
||||||
|
if (updates is TLUpdateShort updateShort)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Short: {updateShort.Update}");
|
||||||
|
if (updateShort.Update is TLUpdateUserStatus status)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User {status.UserId} is {status.Status}");
|
||||||
|
if (status.Status is TLUserStatusOnline)
|
||||||
|
{
|
||||||
|
var peer = new TLInputPeerUser() { UserId = status.UserId };
|
||||||
|
client.SendMessageAsync(peer, "Você está online.").Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (updates is TLUpdateShortMessage message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Message: {message.Message}");
|
||||||
|
MarkMessageRead(client, new TLInputPeerUser() { UserId = message.UserId }, message.Id);
|
||||||
|
}
|
||||||
|
else if (updates is TLUpdateShortChatMessage shortChatMessage)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Chat Message: {shortChatMessage.Message}");
|
||||||
|
MarkMessageRead(client, new TLInputPeerChat() { ChatId = shortChatMessage.ChatId }, shortChatMessage.Id);
|
||||||
|
}
|
||||||
|
else if (updates is TLUpdates allUpdates)
|
||||||
|
{
|
||||||
|
foreach (var update in allUpdates.Updates)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\t{update}");
|
||||||
|
if (update is TLUpdateNewChannelMessage metaMessage)
|
||||||
|
{
|
||||||
|
var channelMsg = metaMessage.Message as TLMessage;
|
||||||
|
Console.WriteLine($"Channel message: {channelMsg.Message}");
|
||||||
|
var channel = allUpdates.Chats[0] as TLChannel;
|
||||||
|
MarkMessageRead(client,
|
||||||
|
new TLInputPeerChannel() { ChannelId = channel.Id, AccessHash = channel.AccessHash.Value },
|
||||||
|
channelMsg.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var user in allUpdates.Users)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{user}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var chat in allUpdates.Chats)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{chat}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkMessageRead(TelegramClient client, TLAbsInputPeer peer, int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new TLRequestReadHistory();
|
||||||
|
request.MaxId = id;
|
||||||
|
request.Peer = peer;
|
||||||
|
client.SendRequestAsync<bool>(request).Wait();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"MarkMessageRead Error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Available Methods
|
# Available Methods
|
||||||
|
|
||||||
For your convenience TLSharp have wrappers for several Telegram API methods. You could add your own, see details below.
|
For your convenience TLSharp have wrappers for several Telegram API methods. You could add your own, see details below.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -24,8 +23,14 @@ namespace TLSharp.Core.Network
|
||||||
private readonly TcpTransport transport;
|
private readonly TcpTransport transport;
|
||||||
private readonly Session session;
|
private readonly Session session;
|
||||||
|
|
||||||
|
private readonly uint UpdatesTooLongID = (uint) new TLUpdatesTooLong().Constructor;
|
||||||
|
|
||||||
public readonly List<ulong> needConfirmation = new List<ulong>();
|
public readonly List<ulong> needConfirmation = new List<ulong>();
|
||||||
|
|
||||||
|
public delegate void HandleUpdates (TLAbsUpdates updates);
|
||||||
|
|
||||||
|
public event HandleUpdates UpdatesEvent;
|
||||||
|
|
||||||
public MtProtoSender(TcpTransport transport, Session session)
|
public MtProtoSender(TcpTransport transport, Session session)
|
||||||
{
|
{
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -60,7 +65,6 @@ namespace TLSharp.Core.Network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
using (var memory = new MemoryStream())
|
using (var memory = new MemoryStream())
|
||||||
using (var writer = new BinaryWriter(memory))
|
using (var writer = new BinaryWriter(memory))
|
||||||
{
|
{
|
||||||
|
|
@ -90,11 +94,12 @@ namespace TLSharp.Core.Network
|
||||||
plaintextWriter.Write(packet.Length);
|
plaintextWriter.Write(packet.Length);
|
||||||
plaintextWriter.Write(packet);
|
plaintextWriter.Write(packet);
|
||||||
|
|
||||||
msgKey = Helpers.CalcMsgKey(plaintextPacket.GetBuffer());
|
var buffer = plaintextPacket.GetBuffer();
|
||||||
ciphertext = AES.EncryptAES(Helpers.CalcKey(session.AuthKey.Data, msgKey, true), plaintextPacket.GetBuffer());
|
msgKey = Helpers.CalcMsgKey(buffer);
|
||||||
|
ciphertext = AES.EncryptAES(Helpers.CalcKey(session.AuthKey.Data, msgKey, true),
|
||||||
|
plaintextPacket.GetBuffer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (MemoryStream ciphertextPacket = makeMemory(8 + 16 + ciphertext.Length))
|
using (MemoryStream ciphertextPacket = makeMemory(8 + 16 + ciphertext.Length))
|
||||||
{
|
{
|
||||||
using (BinaryWriter writer = new BinaryWriter(ciphertextPacket))
|
using (BinaryWriter writer = new BinaryWriter(ciphertextPacket))
|
||||||
|
|
@ -108,6 +113,23 @@ namespace TLSharp.Core.Network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task Ack(CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (needConfirmation.Any())
|
||||||
|
{
|
||||||
|
var ackRequest = new AckRequest(needConfirmation);
|
||||||
|
using (var memory = new MemoryStream())
|
||||||
|
using (var writer = new BinaryWriter(memory))
|
||||||
|
{
|
||||||
|
ackRequest.SerializeBody(writer);
|
||||||
|
await Send(memory.ToArray(), ackRequest);
|
||||||
|
needConfirmation.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Tuple<byte[], ulong, int> DecodeMessage(byte[] body)
|
private Tuple<byte[], ulong, int> DecodeMessage(byte[] body)
|
||||||
{
|
{
|
||||||
byte[] message;
|
byte[] message;
|
||||||
|
|
@ -149,7 +171,7 @@ namespace TLSharp.Core.Network
|
||||||
using (var messageStream = new MemoryStream(result.Item1, false))
|
using (var messageStream = new MemoryStream(result.Item1, false))
|
||||||
using (var messageReader = new BinaryReader(messageStream))
|
using (var messageReader = new BinaryReader(messageStream))
|
||||||
{
|
{
|
||||||
processMessage(result.Item2, result.Item3, messageReader, request, token);
|
await ProcessMessageAsync(result.Item2, result.Item3, messageReader, request, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
@ -158,6 +180,21 @@ namespace TLSharp.Core.Network
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> Receive(TimeSpan timeToWait, CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var result = DecodeMessage((await transport.Receive(timeToWait)).Body);
|
||||||
|
|
||||||
|
using (var messageStream = new MemoryStream(result.Item1, false))
|
||||||
|
using (var messageReader = new BinaryReader(messageStream))
|
||||||
|
{
|
||||||
|
await ProcessMessageAsync(result.Item2, result.Item3, messageReader, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendPingAsync(CancellationToken token = default(CancellationToken))
|
public async Task SendPingAsync(CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
@ -173,7 +210,7 @@ namespace TLSharp.Core.Network
|
||||||
await Receive(pingRequest, token).ConfigureAwait(false);
|
await Receive(pingRequest, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool processMessage(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken))
|
private async Task<bool> ProcessMessageAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|
@ -181,8 +218,10 @@ namespace TLSharp.Core.Network
|
||||||
// TODO: check sessionid
|
// TODO: check sessionid
|
||||||
// TODO: check seqno
|
// TODO: check seqno
|
||||||
|
|
||||||
|
|
||||||
//logger.debug("processMessage: msg_id {0}, sequence {1}, data {2}", BitConverter.ToString(((MemoryStream)messageReader.BaseStream).GetBuffer(), (int) messageReader.BaseStream.Position, (int) (messageReader.BaseStream.Length - messageReader.BaseStream.Position)).Replace("-","").ToLower());
|
//logger.debug("processMessage: msg_id {0}, sequence {1}, data {2}", BitConverter.ToString(((MemoryStream)messageReader.BaseStream).GetBuffer(), (int) messageReader.BaseStream.Position, (int) (messageReader.BaseStream.Length - messageReader.BaseStream.Position)).Replace("-","").ToLower());
|
||||||
needConfirmation.Add(messageId);
|
needConfirmation.Add(messageId);
|
||||||
|
await Ack(token);
|
||||||
|
|
||||||
uint code = messageReader.ReadUInt32();
|
uint code = messageReader.ReadUInt32();
|
||||||
messageReader.BaseStream.Position -= 4;
|
messageReader.BaseStream.Position -= 4;
|
||||||
|
|
@ -190,7 +229,7 @@ namespace TLSharp.Core.Network
|
||||||
{
|
{
|
||||||
case 0x73f1f8dc: // container
|
case 0x73f1f8dc: // container
|
||||||
//logger.debug("MSG container");
|
//logger.debug("MSG container");
|
||||||
return HandleContainer(messageId, sequence, messageReader, request, token);
|
return await HandleContainerAsync(messageId, sequence, messageReader, request, token);
|
||||||
case 0x7abe77ec: // ping
|
case 0x7abe77ec: // ping
|
||||||
//logger.debug("MSG ping");
|
//logger.debug("MSG ping");
|
||||||
return HandlePing(messageId, sequence, messageReader);
|
return HandlePing(messageId, sequence, messageReader);
|
||||||
|
|
@ -208,7 +247,7 @@ namespace TLSharp.Core.Network
|
||||||
return HandleMsgsAck(messageId, sequence, messageReader);
|
return HandleMsgsAck(messageId, sequence, messageReader);
|
||||||
case 0xedab447b: // bad_server_salt
|
case 0xedab447b: // bad_server_salt
|
||||||
//logger.debug("MSG bad_server_salt");
|
//logger.debug("MSG bad_server_salt");
|
||||||
return HandleBadServerSalt(messageId, sequence, messageReader, request, token);
|
return await HandleBadServerSaltAsync(messageId, sequence, messageReader, request, token);
|
||||||
case 0xa7eff811: // bad_msg_notification
|
case 0xa7eff811: // bad_msg_notification
|
||||||
//logger.debug("MSG bad_msg_notification");
|
//logger.debug("MSG bad_msg_notification");
|
||||||
return HandleBadMsgNotification(messageId, sequence, messageReader);
|
return HandleBadMsgNotification(messageId, sequence, messageReader);
|
||||||
|
|
@ -220,39 +259,61 @@ namespace TLSharp.Core.Network
|
||||||
return HandleRpcResult(messageId, sequence, messageReader, request);
|
return HandleRpcResult(messageId, sequence, messageReader, request);
|
||||||
case 0x3072cfa1: // gzip_packed
|
case 0x3072cfa1: // gzip_packed
|
||||||
//logger.debug("MSG gzip_packed");
|
//logger.debug("MSG gzip_packed");
|
||||||
return HandleGzipPacked(messageId, sequence, messageReader, request, token);
|
return await HandleGzipPackedAsync(messageId, sequence, messageReader, request, token);
|
||||||
case 0xe317af7e:
|
case 0xe317af7e:
|
||||||
case 0xd3f45784:
|
case 0xd3f45784:
|
||||||
case 0x2b2fbd4e:
|
case 0x2b2fbd4e:
|
||||||
case 0x78d4dec1:
|
case 0x78d4dec1:
|
||||||
case 0x725b04c3:
|
case 0x725b04c3:
|
||||||
case 0x74ae4240:
|
case 0x74ae4240:
|
||||||
return HandleUpdate(messageId, sequence, messageReader);
|
case 0x11f1331c:
|
||||||
|
return HandleUpdate(code, sequence, messageReader, request);
|
||||||
default:
|
default:
|
||||||
//logger.debug("unknown message: {0}", code);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleUpdate(ulong messageId, int sequence, BinaryReader messageReader)
|
private bool HandleUpdate(uint code, int sequence, BinaryReader messageReader, TLMethod request)
|
||||||
{
|
{
|
||||||
return false;
|
var update = ParseUpdate(code, messageReader);
|
||||||
|
if (update != null && UpdatesEvent != null)
|
||||||
/*
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
UpdatesEvent(TL.Parse<Updates>(messageReader));
|
UpdatesEvent(update);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
|
||||||
|
private TLAbsUpdates ParseUpdate(uint code, BinaryReader messageReader)
|
||||||
{
|
{
|
||||||
logger.warning("update processing exception: {0}", e);
|
switch (code)
|
||||||
return false;
|
{
|
||||||
|
case 0xe317af7e:
|
||||||
|
return DecodeUpdate<TLUpdatesTooLong>(messageReader);
|
||||||
|
case 0x914fbf11:
|
||||||
|
return DecodeUpdate<TLUpdateShortMessage> (messageReader);
|
||||||
|
case 0x16812688:
|
||||||
|
return DecodeUpdate<TLUpdateShortChatMessage> (messageReader);
|
||||||
|
case 0x78d4dec1:
|
||||||
|
return DecodeUpdate<TLUpdateShort> (messageReader);
|
||||||
|
case 0x725b04c3:
|
||||||
|
return DecodeUpdate<TLUpdatesCombined> (messageReader);
|
||||||
|
case 0x74ae4240:
|
||||||
|
return DecodeUpdate<TLUpdates> (messageReader);
|
||||||
|
case 0x11f1331c:
|
||||||
|
return DecodeUpdate<TLUpdateShortSentMessage> (messageReader);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleGzipPacked(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken))
|
private TLAbsUpdates DecodeUpdate<T>(BinaryReader messageReader) where T : TLAbsUpdates
|
||||||
|
{
|
||||||
|
var ms = messageReader.BaseStream as MemoryStream;
|
||||||
|
var update = (T)ObjectUtils.DeserializeObject(messageReader);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HandleGzipPackedAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|
@ -269,7 +330,7 @@ namespace TLSharp.Core.Network
|
||||||
}
|
}
|
||||||
using (BinaryReader compressedReader = new BinaryReader(ms))
|
using (BinaryReader compressedReader = new BinaryReader(ms))
|
||||||
{
|
{
|
||||||
processMessage(messageId, sequence, compressedReader, request, token);
|
await ProcessMessageAsync(messageId, sequence, compressedReader, request, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,6 +366,7 @@ namespace TLSharp.Core.Network
|
||||||
{ // rpc_error
|
{ // rpc_error
|
||||||
int errorCode = messageReader.ReadInt32();
|
int errorCode = messageReader.ReadInt32();
|
||||||
string errorMessage = Serializers.String.Read(messageReader);
|
string errorMessage = Serializers.String.Read(messageReader);
|
||||||
|
Console.Error.WriteLine($"ERROR: {errorMessage} - {errorCode}");
|
||||||
|
|
||||||
if (errorMessage.StartsWith("FLOOD_WAIT_"))
|
if (errorMessage.StartsWith("FLOOD_WAIT_"))
|
||||||
{
|
{
|
||||||
|
|
@ -415,7 +477,7 @@ namespace TLSharp.Core.Network
|
||||||
throw new InvalidOperationException("invalid container");
|
throw new InvalidOperationException("invalid container");
|
||||||
|
|
||||||
}
|
}
|
||||||
throw new NotImplementedException("This should never happens");
|
throw new NotImplementedException("This should never happen!");
|
||||||
/*
|
/*
|
||||||
logger.debug("bad_msg_notification: msgid {0}, seq {1}, errorcode {2}", requestId, requestSequence,
|
logger.debug("bad_msg_notification: msgid {0}, seq {1}, errorcode {2}", requestId, requestSequence,
|
||||||
errorCode);
|
errorCode);
|
||||||
|
|
@ -435,7 +497,7 @@ namespace TLSharp.Core.Network
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleBadServerSalt(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken))
|
private async Task<bool> HandleBadServerSaltAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|
@ -450,7 +512,7 @@ namespace TLSharp.Core.Network
|
||||||
session.Salt = newSalt;
|
session.Salt = newSalt;
|
||||||
|
|
||||||
//resend
|
//resend
|
||||||
Send(request, token);
|
await Send(request, token);
|
||||||
/*
|
/*
|
||||||
if(!runningRequests.ContainsKey(badMsgId)) {
|
if(!runningRequests.ContainsKey(badMsgId)) {
|
||||||
logger.debug("bad server salt on unknown message");
|
logger.debug("bad server salt on unknown message");
|
||||||
|
|
@ -516,7 +578,7 @@ namespace TLSharp.Core.Network
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleContainer(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken))
|
private async Task<bool> HandleContainerAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|
@ -530,11 +592,16 @@ namespace TLSharp.Core.Network
|
||||||
long beginPosition = messageReader.BaseStream.Position;
|
long beginPosition = messageReader.BaseStream.Position;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!processMessage(innerMessageId, sequence, messageReader, request, token))
|
var processedMessage = await ProcessMessageAsync(innerMessageId, sequence, messageReader, request, token);
|
||||||
|
if (!processedMessage)
|
||||||
{
|
{
|
||||||
messageReader.BaseStream.Position = beginPosition + innerLength;
|
messageReader.BaseStream.Position = beginPosition + innerLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException e)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// logger.error("failed to process message in container: {0}", e);
|
// logger.error("failed to process message in container: {0}", e);
|
||||||
|
|
|
||||||
25
TLSharp.Core/Network/Sniffer.cs
Normal file
25
TLSharp.Core/Network/Sniffer.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace TLSharp.Core.Network
|
||||||
|
{
|
||||||
|
public static class Sniffer
|
||||||
|
{
|
||||||
|
public static string MessageOut(byte[] data)
|
||||||
|
{
|
||||||
|
return WriteMessage(new StringBuilder("[OUT]:"), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MessageIn(byte[] data)
|
||||||
|
{
|
||||||
|
return WriteMessage(new StringBuilder("[IN]:"), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WriteMessage(StringBuilder log, byte[] data)
|
||||||
|
{
|
||||||
|
foreach (var b in data)
|
||||||
|
log.AppendFormat(" {0:x2}", b);
|
||||||
|
return log.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ namespace TLSharp.Core.Network
|
||||||
private readonly TcpClient tcpClient;
|
private readonly TcpClient tcpClient;
|
||||||
private readonly NetworkStream stream;
|
private readonly NetworkStream stream;
|
||||||
private int sendCounter = 0;
|
private int sendCounter = 0;
|
||||||
|
private CancellationTokenSource tokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
public TcpTransport(string address, int port, TcpClientConnectionHandler handler = null)
|
public TcpTransport(string address, int port, TcpClientConnectionHandler handler = null)
|
||||||
{
|
{
|
||||||
|
|
@ -54,11 +55,76 @@ namespace TLSharp.Core.Network
|
||||||
|
|
||||||
public async Task<TcpMessage> Receive(CancellationToken token = default(CancellationToken))
|
public async Task<TcpMessage> Receive(CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
|
var stream = tcpClient.GetStream();
|
||||||
|
|
||||||
var packetLengthBytes = new byte[4];
|
var packetLengthBytes = new byte[4];
|
||||||
if (await stream.ReadAsync(packetLengthBytes, 0, 4, token).ConfigureAwait(false) != 4)
|
if (await stream.ReadAsync(packetLengthBytes, 0, 4, token).ConfigureAwait(false) != 4)
|
||||||
throw new InvalidOperationException("Couldn't read the packet length");
|
throw new InvalidOperationException("Couldn't read the packet length");
|
||||||
int packetLength = BitConverter.ToInt32(packetLengthBytes, 0);
|
int packetLength = BitConverter.ToInt32(packetLengthBytes, 0);
|
||||||
|
|
||||||
|
var seqBytes = new byte[4];
|
||||||
|
if (await stream.ReadAsync(seqBytes, 0, 4) != 4)
|
||||||
|
throw new InvalidOperationException("Couldn't read the sequence");
|
||||||
|
int seq = BitConverter.ToInt32(seqBytes, 0);
|
||||||
|
|
||||||
|
int readBytes = 0;
|
||||||
|
var body = new byte[packetLength - 12];
|
||||||
|
int neededToRead = packetLength - 12;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var bodyByte = new byte[packetLength - 12];
|
||||||
|
var availableBytes = await stream.ReadAsync(bodyByte, 0, neededToRead);
|
||||||
|
neededToRead -= availableBytes;
|
||||||
|
Buffer.BlockCopy(bodyByte, 0, body, readBytes, availableBytes);
|
||||||
|
readBytes += availableBytes;
|
||||||
|
}
|
||||||
|
while (readBytes != packetLength - 12);
|
||||||
|
|
||||||
|
var crcBytes = new byte[4];
|
||||||
|
if (await stream.ReadAsync(crcBytes, 0, 4) != 4)
|
||||||
|
throw new InvalidOperationException("Couldn't read the crc");
|
||||||
|
int checksum = BitConverter.ToInt32(crcBytes, 0);
|
||||||
|
|
||||||
|
byte[] rv = new byte[packetLengthBytes.Length + seqBytes.Length + body.Length];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(packetLengthBytes, 0, rv, 0, packetLengthBytes.Length);
|
||||||
|
Buffer.BlockCopy(seqBytes, 0, rv, packetLengthBytes.Length, seqBytes.Length);
|
||||||
|
Buffer.BlockCopy(body, 0, rv, packetLengthBytes.Length + seqBytes.Length, body.Length);
|
||||||
|
var crc32 = new Crc32();
|
||||||
|
var computedChecksum = crc32.ComputeHash(rv).Reverse();
|
||||||
|
|
||||||
|
if (!crcBytes.SequenceEqual(computedChecksum))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("invalid checksum! skip");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TcpMessage(seq, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TcpMessage> Receive(TimeSpan timeToWait)
|
||||||
|
{
|
||||||
|
var stream = tcpClient.GetStream();
|
||||||
|
|
||||||
|
var packetLengthBytes = new byte[4];
|
||||||
|
var token = tokenSource.Token;
|
||||||
|
stream.ReadTimeout = (int)timeToWait.TotalMilliseconds;
|
||||||
|
int bytes = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bytes = stream.Read(packetLengthBytes, 0, 4);
|
||||||
|
}
|
||||||
|
catch (System.IO.IOException io)
|
||||||
|
{
|
||||||
|
var socketError = io.InnerException as SocketException;
|
||||||
|
if (socketError != null && socketError.SocketErrorCode == SocketError.TimedOut)
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
if (bytes != 4)
|
||||||
|
throw new InvalidOperationException("Couldn't read the packet length");
|
||||||
|
int packetLength = BitConverter.ToInt32(packetLengthBytes, 0);
|
||||||
|
|
||||||
var seqBytes = new byte[4];
|
var seqBytes = new byte[4];
|
||||||
if (await stream.ReadAsync(seqBytes, 0, 4, token).ConfigureAwait(false) != 4)
|
if (await stream.ReadAsync(seqBytes, 0, 4, token).ConfigureAwait(false) != 4)
|
||||||
throw new InvalidOperationException("Couldn't read the sequence");
|
throw new InvalidOperationException("Couldn't read the sequence");
|
||||||
|
|
@ -106,7 +172,6 @@ namespace TLSharp.Core.Network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (tcpClient.Connected)
|
if (tcpClient.Connected)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@
|
||||||
<Compile Include="Session.cs" />
|
<Compile Include="Session.cs" />
|
||||||
<Compile Include="TelegramClient.cs" />
|
<Compile Include="TelegramClient.cs" />
|
||||||
<Compile Include="Utils\Helpers.cs" />
|
<Compile Include="Utils\Helpers.cs" />
|
||||||
|
<Compile Include="Network\Sniffer.cs" />
|
||||||
<Compile Include="DataCenter.cs" />
|
<Compile Include="DataCenter.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
@ -25,6 +25,7 @@ namespace TLSharp.Core
|
||||||
public class TelegramClient : IDisposable
|
public class TelegramClient : IDisposable
|
||||||
{
|
{
|
||||||
private MtProtoSender sender;
|
private MtProtoSender sender;
|
||||||
|
private AuthKey key;
|
||||||
private TcpTransport transport;
|
private TcpTransport transport;
|
||||||
private string apiHash = String.Empty;
|
private string apiHash = String.Empty;
|
||||||
private int apiId = 0;
|
private int apiId = 0;
|
||||||
|
|
@ -33,6 +34,15 @@ namespace TLSharp.Core
|
||||||
private TcpClientConnectionHandler handler;
|
private TcpClientConnectionHandler handler;
|
||||||
private DataCenterIPVersion dcIpVersion;
|
private DataCenterIPVersion dcIpVersion;
|
||||||
|
|
||||||
|
private bool looping = true;
|
||||||
|
|
||||||
|
public delegate void UpdatesEvent (TelegramClient source, TLAbsUpdates updates);
|
||||||
|
public delegate void ClientEvent(TelegramClient source);
|
||||||
|
|
||||||
|
public event UpdatesEvent Updates;
|
||||||
|
public event ClientEvent ScheduledTasks;
|
||||||
|
public event ClientEvent IdleTasks;
|
||||||
|
|
||||||
public Session Session
|
public Session Session
|
||||||
{
|
{
|
||||||
get { return session; }
|
get { return session; }
|
||||||
|
|
@ -80,6 +90,7 @@ namespace TLSharp.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
sender = new MtProtoSender(transport, session);
|
sender = new MtProtoSender(transport, session);
|
||||||
|
sender.UpdatesEvent += SenderUpdatesEvent;
|
||||||
|
|
||||||
//set-up layer
|
//set-up layer
|
||||||
var config = new TLRequestGetConfig();
|
var config = new TLRequestGetConfig();
|
||||||
|
|
@ -149,6 +160,50 @@ namespace TLSharp.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
looping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MainLoopAsync(TimeSpan timeToWait, CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var lastPing = DateTime.UtcNow;
|
||||||
|
await SendPingAsync();
|
||||||
|
while (looping)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WaitEventAsync(timeToWait, token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Handle timeout, no problem
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((now - lastPing).TotalSeconds >= 30)
|
||||||
|
{
|
||||||
|
await SendPingAsync();
|
||||||
|
lastPing = now;
|
||||||
|
}
|
||||||
|
if (ScheduledTasks != null)
|
||||||
|
{
|
||||||
|
ScheduledTasks.Invoke(this);
|
||||||
|
ScheduledTasks = null;
|
||||||
|
}
|
||||||
|
IdleTasks?.Invoke(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SenderUpdatesEvent (TLAbsUpdates updates)
|
||||||
|
{
|
||||||
|
Updates?.Invoke(this, updates);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RequestWithDcMigration(TLMethod request, CancellationToken token = default(CancellationToken))
|
private async Task RequestWithDcMigration(TLMethod request, CancellationToken token = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (sender == null)
|
if (sender == null)
|
||||||
|
|
@ -178,6 +233,11 @@ namespace TLSharp.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task WaitEventAsync(TimeSpan timeToWait, CancellationToken token = default(CancellationToken))
|
||||||
|
{
|
||||||
|
await sender.Receive (timeToWait, token);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsUserAuthorized()
|
public bool IsUserAuthorized()
|
||||||
{
|
{
|
||||||
return session.TLUser != null;
|
return session.TLUser != null;
|
||||||
|
|
|
||||||
|
|
@ -81,5 +81,11 @@ namespace TLSharp.Tests
|
||||||
{
|
{
|
||||||
await base.SendMessageByUserNameTest();
|
await base.SendMessageByUserNameTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public override async Task GetUpdatesForUser()
|
||||||
|
{
|
||||||
|
await base.GetUpdatesForUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,5 +79,12 @@ namespace TLSharp.Tests
|
||||||
{
|
{
|
||||||
await base.SendMessageByUserNameTest();
|
await base.SendMessageByUserNameTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public override async Task GetUpdatesForUser()
|
||||||
|
{
|
||||||
|
await base.GetUpdatesForUser();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
@ -11,7 +12,6 @@ using TeleSharp.TL;
|
||||||
using TeleSharp.TL.Messages;
|
using TeleSharp.TL.Messages;
|
||||||
using TLSharp.Core;
|
using TLSharp.Core;
|
||||||
using TLSharp.Core.Exceptions;
|
using TLSharp.Core.Exceptions;
|
||||||
using TLSharp.Core.Network;
|
|
||||||
using TLSharp.Core.Network.Exceptions;
|
using TLSharp.Core.Network.Exceptions;
|
||||||
using TLSharp.Core.Utils;
|
using TLSharp.Core.Utils;
|
||||||
|
|
||||||
|
|
@ -377,5 +377,72 @@ namespace TLSharp.Tests
|
||||||
Thread.Sleep(3000);
|
Thread.Sleep(3000);
|
||||||
await client.SendMessageAsync(new TLInputPeerUser() { UserId = user.Id }, "TEST");
|
await client.SendMessageAsync(new TLInputPeerUser() { UserId = user.Id }, "TEST");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task GetUpdatesForUser()
|
||||||
|
{
|
||||||
|
IList<TLMessage> newMsgs = new List<TLMessage>();
|
||||||
|
TLUser user = null;
|
||||||
|
var updateMsg = "Send yourself an UPDATE_1 message to trigger update during loop";
|
||||||
|
|
||||||
|
var client = NewClient();
|
||||||
|
await client.ConnectAsync();
|
||||||
|
|
||||||
|
if (client.IsUserAuthorized())
|
||||||
|
user = client.Session.TLUser;
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var hash = await client.SendCodeRequestAsync(NumberToAuthenticate);
|
||||||
|
var code = CodeToAuthenticate; // you can change code in debugger too
|
||||||
|
if (string.IsNullOrWhiteSpace(code))
|
||||||
|
{
|
||||||
|
throw new Exception("CodeToAuthenticate is empty in the app.config file, fill it with the code you just got now by SMS/Telegram");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user = await client.MakeAuthAsync(NumberToAuthenticate, hash, code);
|
||||||
|
}
|
||||||
|
catch (CloudPasswordNeededException)
|
||||||
|
{
|
||||||
|
var passwordSetting = await client.GetPasswordSetting();
|
||||||
|
var password = PasswordToAuthenticate;
|
||||||
|
user = await client.MakeAuthWithPasswordAsync(passwordSetting, password);
|
||||||
|
}
|
||||||
|
catch (InvalidPhoneCodeException ex)
|
||||||
|
{
|
||||||
|
throw new Exception("CodeToAuthenticate is wrong in the app.config file, fill it with the code you just got now by SMS/Telegram", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Things to note:- If the updates are not getting triggered, please re-authenticate the user
|
||||||
|
// Would trigger the updates on a seperate thread if possible
|
||||||
|
|
||||||
|
client.Updates += (TelegramClient tclient, TLAbsUpdates updates) =>
|
||||||
|
{
|
||||||
|
if (updates is TLUpdates)
|
||||||
|
{
|
||||||
|
var allUpdates = updates as TLUpdates;
|
||||||
|
|
||||||
|
foreach (var update in allUpdates.Updates)
|
||||||
|
{
|
||||||
|
if (update is TLUpdateNewMessage)
|
||||||
|
{
|
||||||
|
var metaMsg = update as TLUpdateNewMessage;
|
||||||
|
var msg = metaMsg.Message as TLMessage;
|
||||||
|
newMsgs.Add(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(updateMsg);
|
||||||
|
Debug.WriteLine(updateMsg);
|
||||||
|
|
||||||
|
await client.MainLoopAsync(new TimeSpan(0, 0, 1));
|
||||||
|
|
||||||
|
Assert.IsTrue(newMsgs.Count == 1);
|
||||||
|
Assert.IsTrue(newMsgs.First().Message.Equals("UPDATE_1"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue