diff --git a/README.md b/README.md index 16c95d6..727af52 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,257 @@ 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) +# Events Sample code +```csharp + using System; + using System.Threading.Tasks; + using TeleSharp.TL; + using TLSharp.Core; + using System.Linq; + using TeleSharp.TL.Messages; + using System.Collections.Generic; + + 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; + try + { + // -- if necessary, IP can be changed so the client can connect to the test network. + Session session = null; + // new Session(new FileSessionStore(), "session") + //{ + // ServerAddress = "149.154.175.10", + // Port = 443 + //}; + //Console.WriteLine($"{session.ServerAddress}:{session.Port} {phone}"); + client = new TelegramClient(APIId, APIHash, session); + // subscribe an event to receive live messages + client.Updates += Client_Updates; + 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()) + { + 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()) + { + var channel = channelObj as TLChannel; + Console.WriteLine($"\tChat: {channel.Title}"); + } + + Console.WriteLine("Groups:"); + TLChat chat = null; + foreach (var chatObj in dialogs.Chats.OfType()) + { + chat = chatObj as TLChat; + Console.WriteLine($"Chat name: {chat.Title}"); + var request = new TLRequestGetFullChat() { ChatId = chat.Id }; + var fullChat = await client.SendRequestAsync(request); + + var participants = (fullChat.FullChat as TeleSharp.TL.TLChatFull).Participants as TLChatParticipants; + foreach (var p in participants.Participants) + { + if (p is TLChatParticipant) + { + var participant = p as TLChatParticipant; + Console.WriteLine($"\t{participant.UserId}"); + } + else if (p is TLChatParticipantAdmin) + { + var participant = p as TLChatParticipantAdmin; + Console.WriteLine($"\t{participant.UserId}**"); + } + else if (p is TLChatParticipantCreator) + { + var participant = p as TLChatParticipantCreator; + Console.WriteLine($"\t{participant.UserId}**"); + } + } + + var peer = new TLInputPeerChat() { ChatId = chat.Id }; + var m = await client.GetHistoryAsync(peer, 0, 0, 0); + Console.WriteLine(m); + if (m is TLMessages) + { + var messages = m as TLMessages; + + + foreach (var message in messages.Messages) + { + if (message is TLMessage) + { + var m1 = message as TLMessage; + Console.WriteLine($"\t\t{m1.Id} {m1.Message}"); + } + else if (message is TLMessageService) + { + var m1 = message as TLMessageService; + Console.WriteLine($"\t\t{m1.Id} {m1.Action}"); + } + } + } + else if (m is TLMessagesSlice) + { + bool done = false; + int total = 0; + while (!done) + { + var messages = m as TLMessagesSlice; + + foreach (var m1 in messages.Messages) + { + if (m1 is TLMessage) + { + var message = m1 as TLMessage; + Console.WriteLine($"\t\t{message.Id} {message.Message}"); + ++total; + } + else if (m1 is TLMessageService) + { + var message = m1 as TLMessageService; + Console.WriteLine($"\t\t{message.Id} {message.Action}"); + ++total; + done = message.Action is TLMessageActionChatCreate; + } + } + m = await client.GetHistoryAsync(peer, total, 0, 0); + } + } + } + + // -- Wait in a loop to handle incoming updates. No need to poll. + for (;;) + { + await client.WaitEventAsync(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private void Client_Updates(TelegramClient client, TLAbsUpdates updates) + { + Console.WriteLine($"Got update: {updates}"); + if (updates is TLUpdateShort) + { + var updateShort = updates as TLUpdateShort; + Console.WriteLine($"Short: {updateShort.Update}"); + if (updateShort.Update is TLUpdateUserStatus) + { + var status = updateShort.Update as TLUpdateUserStatus; + Console.WriteLine($"User {status.UserId} is {status.Status}"); + if (status.Status is TLUserStatusOnline) + { + try + { + var peer = new TLInputPeerUser() { UserId = status.UserId }; + client.SendMessageAsync(peer, "Você está online.").Wait(); + } catch {} + } + } + } + else if (updates is TLUpdateShortMessage) + { + var message = updates as TLUpdateShortMessage; + Console.WriteLine($"Message: {message.Message}"); + MarkMessageRead(client, new TLInputPeerUser() { UserId = message.UserId }, message.Id); + } + else if (updates is TLUpdateShortChatMessage) + { + var message = updates as TLUpdateShortChatMessage; + Console.WriteLine($"Chat Message: {message.Message}"); + MarkMessageRead(client, new TLInputPeerChat() { ChatId = message.ChatId }, message.Id); + } + else if (updates is TLUpdates) + { + var allUpdates = updates as TLUpdates; + foreach (var update in allUpdates.Updates) + { + Console.WriteLine($"\t{update}"); + if (update is TLUpdateNewChannelMessage) + { + var metaMessage = update as TLUpdateNewChannelMessage; + var message = metaMessage.Message as TLMessage; + Console.WriteLine($"Channel message: {message.Message}"); + var channel = allUpdates.Chats[0] as TLChannel; + MarkMessageRead(client, + new TLInputPeerChannel() { ChannelId = channel.Id, AccessHash = channel.AccessHash.Value }, + message.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) + { + // An exception happens here but it's not fatal. + try + { + var request = new TLRequestReadHistory(); + request.MaxId = id; + request.Peer = peer; + client.SendRequestAsync(request).Wait(); + } + catch (InvalidOperationException e){ + System.Console.WriteLine($"MarkMessageRead Error: {e.getMessage()}") + } + + } + } + } +``` + # Available Methods For your convenience TLSharp have wrappers for several Telegram API methods. You could add your own, see details below. diff --git a/TLSharp.Core/Network/Exceptions.cs b/TLSharp.Core/Network/Exceptions.cs new file mode 100644 index 0000000..878e64e --- /dev/null +++ b/TLSharp.Core/Network/Exceptions.cs @@ -0,0 +1,69 @@ +using System; +namespace TLSharp.Core.Network +{ + public class FloodException : Exception + { + public TimeSpan TimeToWait { get; private set; } + + internal FloodException(TimeSpan timeToWait) + : base($"Flood prevention. Telegram now requires your program to do requests again only after {timeToWait.TotalSeconds} seconds have passed ({nameof(TimeToWait)} property)." + + " If you think the culprit of this problem may lie in TLSharp's implementation, open a Github issue please.") + { + TimeToWait = timeToWait; + } + } + + public class BadMessageException : Exception + { + internal BadMessageException(string description) : base(description) + { + } + } + + internal abstract class DataCenterMigrationException : Exception + { + internal int DC { get; private set; } + + private const string REPORT_MESSAGE = + " See: https://github.com/sochix/TLSharp#i-get-a-xxxmigrationexception-or-a-migrate_x-error"; + + protected DataCenterMigrationException(string msg, int dc) : base(msg + REPORT_MESSAGE) + { + DC = dc; + } + } + + internal class PhoneMigrationException : DataCenterMigrationException + { + internal PhoneMigrationException(int dc) + : base($"Phone number registered to a different DC: {dc}.", dc) + { + } + } + + internal class FileMigrationException : DataCenterMigrationException + { + internal FileMigrationException(int dc) + : base($"File located on a different DC: {dc}.", dc) + { + } + } + + internal class UserMigrationException : DataCenterMigrationException + { + internal UserMigrationException(int dc) + : base($"User located on a different DC: {dc}.", dc) + { + } + } + + internal class NetworkMigrationException : DataCenterMigrationException + { + internal NetworkMigrationException(int dc) + : base($"Network located on a different DC: {dc}.", dc) + { + } + } + + +} diff --git a/TLSharp.Core/Network/MtProtoSender.cs b/TLSharp.Core/Network/MtProtoSender.cs index 91e4457..cd010e9 100644 --- a/TLSharp.Core/Network/MtProtoSender.cs +++ b/TLSharp.Core/Network/MtProtoSender.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; @@ -11,7 +10,6 @@ using TeleSharp.TL; using TLSharp.Core.Exceptions; using TLSharp.Core.MTProto; using TLSharp.Core.MTProto.Crypto; -using TLSharp.Core.Network.Exceptions; using TLSharp.Core.Network.Requests; using TLSharp.Core.Utils; @@ -24,8 +22,14 @@ namespace TLSharp.Core.Network private readonly TcpTransport transport; private readonly Session session; + private readonly uint UpdatesTooLongID = (uint) new TLUpdatesTooLong().Constructor; + public readonly List needConfirmation = new List(); + public delegate void HandleUpdates (TLAbsUpdates updates); + + public event HandleUpdates UpdatesEvent; + public MtProtoSender(TcpTransport transport, Session session) { this.transport = transport; @@ -37,7 +41,7 @@ namespace TLSharp.Core.Network return confirmed ? session.Sequence++ * 2 + 1 : session.Sequence * 2; } - public async Task Send(TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) + public async Task Send(TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -54,7 +58,6 @@ namespace TLSharp.Core.Network } } - using (var memory = new MemoryStream()) using (var writer = new BinaryWriter(memory)) { @@ -65,7 +68,7 @@ namespace TLSharp.Core.Network session.Save(); } - public async Task Send(byte[] packet, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) + public async Task Send(byte[] packet, TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -84,11 +87,12 @@ namespace TLSharp.Core.Network plaintextWriter.Write(packet.Length); plaintextWriter.Write(packet); - msgKey = Helpers.CalcMsgKey(plaintextPacket.GetBuffer()); - ciphertext = AES.EncryptAES(Helpers.CalcKey(session.AuthKey.Data, msgKey, true), plaintextPacket.GetBuffer()); + var buffer = 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 (BinaryWriter writer = new BinaryWriter(ciphertextPacket)) @@ -102,6 +106,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 DecodeMessage(byte[] body) { byte[] message; @@ -134,16 +155,16 @@ namespace TLSharp.Core.Network return new Tuple(message, remoteMessageId, remoteSequence); } - public async Task Receive(TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) + public async Task Receive(TLMethod request, CancellationToken token = default(CancellationToken)) { - while (!request.ConfirmReceived) + while (!request.ConfirmReceived) { var result = DecodeMessage((await transport.Receive(token).ConfigureAwait(false)).Body); - using (var messageStream = new MemoryStream(result.Item1, false)) - using (var messageReader = new BinaryReader(messageStream)) + using (var messageStream = new MemoryStream (result.Item1, false)) + 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(); @@ -152,6 +173,21 @@ namespace TLSharp.Core.Network return null; } + public async Task Receive(int timeoutms, CancellationToken token = default(CancellationToken)) + { + var result = DecodeMessage((await transport.Receieve(timeoutms)).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)) { token.ThrowIfCancellationRequested(); @@ -167,7 +203,7 @@ namespace TLSharp.Core.Network await Receive(pingRequest, token).ConfigureAwait(false); } - private bool processMessage(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken)) + private async Task processMessageAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -175,8 +211,10 @@ namespace TLSharp.Core.Network // TODO: check sessionid // 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()); needConfirmation.Add(messageId); + await Ack(token); uint code = messageReader.ReadUInt32(); messageReader.BaseStream.Position -= 4; @@ -184,7 +222,7 @@ namespace TLSharp.Core.Network { case 0x73f1f8dc: // container //logger.debug("MSG container"); - return HandleContainer(messageId, sequence, messageReader, request, token); + return await HandleContainerAsync(messageId, sequence, messageReader, request, token); case 0x7abe77ec: // ping //logger.debug("MSG ping"); return HandlePing(messageId, sequence, messageReader); @@ -202,7 +240,7 @@ namespace TLSharp.Core.Network return HandleMsgsAck(messageId, sequence, messageReader); case 0xedab447b: // 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 //logger.debug("MSG bad_msg_notification"); return HandleBadMsgNotification(messageId, sequence, messageReader); @@ -214,39 +252,68 @@ namespace TLSharp.Core.Network return HandleRpcResult(messageId, sequence, messageReader, request); case 0x3072cfa1: // 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 0xd3f45784: case 0x2b2fbd4e: case 0x78d4dec1: case 0x725b04c3: case 0x74ae4240: - return HandleUpdate(messageId, sequence, messageReader); + case 0x11f1331c: + return HandleUpdate(code, sequence, messageReader, request); default: - //logger.debug("unknown message: {0}", code); return false; } } - private bool HandleUpdate(ulong messageId, int sequence, BinaryReader messageReader) + private bool HandleUpdate(uint code, int sequence, BinaryReader messageReader, TLMethod request) { - return false; - - /* try { - UpdatesEvent(TL.Parse(messageReader)); - return true; + var update = ParseUpdate (code, messageReader); + if (update != null && UpdatesEvent != null) + { + UpdatesEvent (update); + } + return true; } catch (Exception e) { - logger.warning("update processing exception: {0}", e); - return false; - } - */ + return false; + } } - private bool HandleGzipPacked(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken)) + private TLAbsUpdates ParseUpdate(uint code, BinaryReader messageReader) + { + switch (code) + { + case 0xe317af7e: + return DecodeUpdate(messageReader); + case 0x914fbf11: + return DecodeUpdate (messageReader); + case 0x16812688: + return DecodeUpdate (messageReader); + case 0x78d4dec1: + return DecodeUpdate (messageReader); + case 0x725b04c3: + return DecodeUpdate (messageReader); + case 0x74ae4240: + return DecodeUpdate (messageReader); + case 0x11f1331c: + return DecodeUpdate (messageReader); + default: + return null; + } + } + + private TLAbsUpdates DecodeUpdate(BinaryReader messageReader) where T : TLAbsUpdates + { + var ms = messageReader.BaseStream as MemoryStream; + var update = (T)ObjectUtils.DeserializeObject(messageReader); + return update; + } + + private async Task HandleGzipPackedAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -263,14 +330,14 @@ namespace TLSharp.Core.Network } using (BinaryReader compressedReader = new BinaryReader(ms)) { - processMessage(messageId, sequence, compressedReader, request, token); + await processMessageAsync(messageId, sequence, compressedReader, request, token); } } return true; } - private bool HandleRpcResult(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request) + private bool HandleRpcResult(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request) { uint code = messageReader.ReadUInt32(); ulong requestId = messageReader.ReadUInt64(); @@ -299,6 +366,7 @@ namespace TLSharp.Core.Network { // rpc_error int errorCode = messageReader.ReadInt32(); string errorMessage = Serializers.String.Read(messageReader); + Console.Error.WriteLine($"ERROR: {errorMessage} - {errorCode}"); if (errorMessage.StartsWith("FLOOD_WAIT_")) { @@ -409,7 +477,7 @@ namespace TLSharp.Core.Network 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, errorCode); @@ -429,7 +497,7 @@ namespace TLSharp.Core.Network return true; } - private bool HandleBadServerSalt(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken)) + private async Task HandleBadServerSaltAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -444,7 +512,7 @@ namespace TLSharp.Core.Network session.Salt = newSalt; //resend - Send(request, token); + await Send(request, token); /* if(!runningRequests.ContainsKey(badMsgId)) { logger.debug("bad server salt on unknown message"); @@ -492,7 +560,7 @@ namespace TLSharp.Core.Network return true; } - private bool HandlePong(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request) + private bool HandlePong(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request) { uint code = messageReader.ReadUInt32(); ulong msgId = messageReader.ReadUInt64(); @@ -510,7 +578,7 @@ namespace TLSharp.Core.Network return false; } - private bool HandleContainer(ulong messageId, int sequence, BinaryReader messageReader, TLMethod request, CancellationToken token = default(CancellationToken)) + private async Task HandleContainerAsync(ulong messageId, int sequence, BinaryReader messageReader, TeleSharp.TL.TLMethod request, CancellationToken token = default(CancellationToken)) { token.ThrowIfCancellationRequested(); @@ -524,11 +592,16 @@ namespace TLSharp.Core.Network long beginPosition = messageReader.BaseStream.Position; try { - if (!processMessage(innerMessageId, sequence, messageReader, request, token)) + var processedMessage = await processMessageAsync(innerMessageId, sequence, messageReader, request, token); + if (!processedMessage) { messageReader.BaseStream.Position = beginPosition + innerLength; } } + catch (InvalidOperationException e) + { + throw e; + } catch (Exception e) { // logger.error("failed to process message in container: {0}", e); diff --git a/TLSharp.Core/Network/Sniffer.cs b/TLSharp.Core/Network/Sniffer.cs new file mode 100644 index 0000000..a1033bc --- /dev/null +++ b/TLSharp.Core/Network/Sniffer.cs @@ -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(); + } + } +} diff --git a/TLSharp.Core/Network/TcpTransport.cs b/TLSharp.Core/Network/TcpTransport.cs index cfc7956..306501e 100644 --- a/TLSharp.Core/Network/TcpTransport.cs +++ b/TLSharp.Core/Network/TcpTransport.cs @@ -15,6 +15,7 @@ namespace TLSharp.Core.Network private readonly TcpClient tcpClient; private readonly NetworkStream stream; private int sendCounter = 0; + private CancellationTokenSource tokenSource = new CancellationTokenSource(); public TcpTransport(string address, int port, TcpClientConnectionHandler handler = null) { @@ -54,11 +55,75 @@ namespace TLSharp.Core.Network public async Task Receive(CancellationToken token = default(CancellationToken)) { + var stream = tcpClient.GetStream(); + var packetLengthBytes = new byte[4]; if (await stream.ReadAsync(packetLengthBytes, 0, 4, token).ConfigureAwait(false) != 4) throw new InvalidOperationException("Couldn't read the packet length"); 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 Receieve(int timeoutms) + { + var stream = tcpClient.GetStream(); + + var packetLengthBytes = new byte[4]; + var token = tokenSource.Token; + stream.ReadTimeout = timeoutms; + 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 io; + } + if (bytes != 4) + throw new InvalidOperationException("Couldn't read the packet length"); + int packetLength = BitConverter.ToInt32(packetLengthBytes, 0); + var seqBytes = new byte[4]; if (await stream.ReadAsync(seqBytes, 0, 4, token).ConfigureAwait(false) != 4) throw new InvalidOperationException("Couldn't read the sequence"); @@ -106,7 +171,6 @@ namespace TLSharp.Core.Network } } - public void Dispose() { if (tcpClient.Connected) diff --git a/TLSharp.Core/TLSharp.Core.csproj b/TLSharp.Core/TLSharp.Core.csproj index 9924ab2..52d1966 100644 --- a/TLSharp.Core/TLSharp.Core.csproj +++ b/TLSharp.Core/TLSharp.Core.csproj @@ -75,6 +75,8 @@ + + diff --git a/TLSharp.Core/TelegramClient.cs b/TLSharp.Core/TelegramClient.cs index e5b0819..13b76ba 100644 --- a/TLSharp.Core/TelegramClient.cs +++ b/TLSharp.Core/TelegramClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -16,7 +16,6 @@ using TLSharp.Core.Auth; using TLSharp.Core.Exceptions; using TLSharp.Core.MTProto.Crypto; using TLSharp.Core.Network; -using TLSharp.Core.Network.Exceptions; using TLSharp.Core.Utils; using TLAuthorization = TeleSharp.TL.Auth.TLAuthorization; @@ -25,14 +24,24 @@ namespace TLSharp.Core public class TelegramClient : IDisposable { private MtProtoSender sender; + private AuthKey _key; private TcpTransport transport; - private string apiHash = String.Empty; + private string apiHash = ""; private int apiId = 0; private Session session; private List dcOptions; private TcpClientConnectionHandler handler; 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 { get { return session; } @@ -80,6 +89,7 @@ namespace TLSharp.Core } sender = new MtProtoSender(transport, session); + sender.UpdatesEvent += SenderUpdatesEvent; //set-up layer var config = new TLRequestGetConfig(); @@ -149,6 +159,50 @@ namespace TLSharp.Core } } + public void Close() + { + _looping = false; + } + + public async Task MainLoopAsync(int timeslicems, CancellationToken token = default(CancellationToken)) + { + var lastPing = DateTime.UtcNow; + await SendPingAsync(); + while (_looping) + { + try + { + await WaitEventAsync(timeslicems, 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)) { if (sender == null) @@ -178,6 +232,11 @@ namespace TLSharp.Core } } + public async Task WaitEventAsync(int timeoutms, CancellationToken token = default(CancellationToken)) + { + await sender.Receive (timeoutms, token); + } + public bool IsUserAuthorized() { return session.TLUser != null; diff --git a/TLSharp.Core/packages.config b/TLSharp.Core/packages.config index 6b8deb9..518c7dc 100644 --- a/TLSharp.Core/packages.config +++ b/TLSharp.Core/packages.config @@ -1,3 +1,5 @@  + + \ No newline at end of file diff --git a/TLSharp.Tests.NUnit/Test.cs b/TLSharp.Tests.NUnit/Test.cs index 40e54ac..3696b07 100644 --- a/TLSharp.Tests.NUnit/Test.cs +++ b/TLSharp.Tests.NUnit/Test.cs @@ -81,5 +81,11 @@ namespace TLSharp.Tests { await base.SendMessageByUserNameTest(); } + + [Test] + public override async Task GetUpdatesForUser() + { + await base.GetUpdatesForUser(); + } } } diff --git a/TLSharp.Tests.VS/TLSharpTestsVs.cs b/TLSharp.Tests.VS/TLSharpTestsVs.cs index 8f6c58d..ccb7ea7 100644 --- a/TLSharp.Tests.VS/TLSharpTestsVs.cs +++ b/TLSharp.Tests.VS/TLSharpTestsVs.cs @@ -79,5 +79,12 @@ namespace TLSharp.Tests { await base.SendMessageByUserNameTest(); } + + [TestMethod] + public override async Task GetUpdatesForUser() + { + await base.GetUpdatesForUser(); + } + } } diff --git a/TLSharp.Tests/TLSharpTests.cs b/TLSharp.Tests/TLSharpTests.cs index 6acde72..e54c131 100644 --- a/TLSharp.Tests/TLSharpTests.cs +++ b/TLSharp.Tests/TLSharpTests.cs @@ -1,5 +1,6 @@  using System; +using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.IO; @@ -11,8 +12,6 @@ using TeleSharp.TL; using TeleSharp.TL.Messages; using TLSharp.Core; using TLSharp.Core.Exceptions; -using TLSharp.Core.Network; -using TLSharp.Core.Network.Exceptions; using TLSharp.Core.Utils; namespace TLSharp.Tests @@ -129,7 +128,7 @@ namespace TLSharp.Tests var hash = await client.SendCodeRequestAsync(NumberToAuthenticate); var code = CodeToAuthenticate; // you can change code in debugger too - if (String.IsNullOrWhiteSpace(code)) + 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"); } @@ -178,7 +177,7 @@ namespace TLSharp.Tests if (user == null) { - throw new System.Exception("Number was not found in Contacts List of user: " + NumberToSendMessage); + throw new Exception("Number was not found in Contacts List of user: " + NumberToSendMessage); } await client.SendTypingAsync(new TLInputPeerUser() { UserId = user.Id }); @@ -192,7 +191,7 @@ namespace TLSharp.Tests await client.ConnectAsync(); - var dialogs = (TLDialogs) await client.GetUserDialogsAsync(); + var dialogs = (TLDialogs)await client.GetUserDialogsAsync(); var chat = dialogs.Chats .OfType() .FirstOrDefault(c => c.Title == "TestGroup"); @@ -269,7 +268,7 @@ namespace TLSharp.Tests Version = document.Version }, document.Size); - + Assert.IsTrue(resFile.Bytes.Length > 0); } @@ -284,9 +283,9 @@ namespace TLSharp.Tests var user = result.Users .OfType() .FirstOrDefault(x => x.Id == 5880094); - + var photo = ((TLUserProfilePhoto)user.Photo); - var photoLocation = (TLFileLocation) photo.PhotoBig; + var photoLocation = (TLFileLocation)photo.PhotoBig; var resFile = await client.GetFile(new TLInputFileLocation() { @@ -295,7 +294,7 @@ namespace TLSharp.Tests VolumeId = photoLocation.VolumeId }, 1024); - var res = await client.GetUserDialogsAsync(); + var res = await client.GetUserDialogsAsync(); Assert.IsTrue(resFile.Bytes.Length > 0); } @@ -333,7 +332,7 @@ namespace TLSharp.Tests { await CheckPhones(); } - catch (FloodException floodException) + catch (Core.Network.Exceptions.FloodException floodException) { Console.WriteLine($"FLOODEXCEPTION: {floodException}"); Thread.Sleep(floodException.TimeToWait); @@ -370,12 +369,74 @@ namespace TLSharp.Tests if (user == null) { - throw new System.Exception("Username was not found: " + UserNameToSendMessage); + throw new Exception("Username was not found: " + UserNameToSendMessage); } await client.SendTypingAsync(new TLInputPeerUser() { UserId = user.Id }); Thread.Sleep(3000); await client.SendMessageAsync(new TLInputPeerUser() { UserId = user.Id }, "TEST"); } + + public virtual async Task GetUpdatesForUser() + { + IList newMsgs = new List(); + TLUser user = null; + + 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); + } + } + + 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); + } + } + } + }; + + await client.MainLoopAsync(1000); + + // At this point you would send yourself a UPDATE_1 message to trigger update + + Assert.IsTrue(newMsgs.Count == 1); + Assert.IsTrue(newMsgs.First().Message.Equals("UPDATE_1")); + } } -} +} \ No newline at end of file