Update handling in WTelegramClient
Ali Salehi
Posted on January 9, 2024
in the previous article we talked about starting to work with WTC and setting up the basic requirements. in this article we are going through the process of handling updates for beginners. lets get started.
setup
we have our basic hello world code from previous article here:
using var client = new WTelegram.Client(Config);
await client.LoginUserIfNeeded();
return;
static string? Config(string what)
{
switch (what)
{
case "session_pathname": return Path.Join(Environment.CurrentDirectory, "mysessionFILE.session");
case "api_id": return "YOUR_API_ID";
case "api_hash": return "YOUR_API_HASH";
case "phone_number": return "+12025550156";
case "verification_code": Console.Write("Code: "); return Console.ReadLine()!;
case "first_name": return "John"; // if sign-up is required
case "last_name": return "Doe"; // if sign-up is required
case "password": return "secret!"; // if user has enabled 2FA
default: return null;
}
}
don't forget to replace your phone number, your api_id & api_hash and also your 2FA password.
subscribing to updates
the first thing we need to do, is to tell the WTC that we want to listen to the incoming updates, to do that we simply subscribe to an Event called OnUpdate
:
client.OnUpdate += UpdateHandler;
async Task UpdateHandler(UpdatesBase updatesBase) {
return;
}
lets talk about what just happened here, first we have our client
which at this point "might" be logged-in, we don't know yet, when we are subscribing (+=
) to an Event which gives us an object of type UpdatesBase
and needs us to return a Task
, basically a Func<UpdatesBase, Task>
(we'll talk about that in the future) and then we are returning right after the subscription.
if your code is not async you can do
return Task.CompletedTask;
and remove theasync
.
Updates
Telegram Type system by many people is considered Complex, or Confusing, and maybe it is, you just have to work with it for a relatively long time (that literally the definition of complexity but whatever). but I believe it would be much much more complex IF we didn't have pattern-matching
in C#, BUT WE DO!
lets look at the basic types in WTC.
first we have something called UpdatesBase
, It derives from IObject
. an empty interface, this is due to serialization reasons, but you don't need to worry about this, you just need know this:
every Telegram object inside the WTC, drives from
IObject
.
lets look at its fields:
Update[] UpdateList;
Dictionary<long, User> Users;
Dictionary<long, ChatBase> Chats;
DateTime Date;
we are going to talk about the most important one: UpdateList
,
as the name says, this is an list of Update
.
all the updates that we need are going to be inside this array.
if you receive one message, this is going to be an array with one item inside it, if you get 10 messages in a very short period of time, lets say in 200 milli seconds
, this array is going to be filled with 10
items.
but if you are getting 1 message every seconds, your UpdateHandler
will be called 10 times with 1 message each time.
so it is important to check the whole array of update and not just the first
or last
item.
so we'll write:
async Task UpdateHandler(UpdatesBase updatesBase)
{
foreach (Update? update in updatesBase.UpdateList)
{
}
}
take a closer look, the update
variable is an Update?
. what is it?
public abstract class Update : IObject { }
this is very similar to the concept of IObject
, as you can see its an abstract
class that has no items inside it, just like the IObject
, this is the base definition of every Update inside the WTC.
remember that I said:
every object is an IObject
.
but also every update drives from Update
, for the same reasons!
so what should we do with it ? this is where the pattern-matching
comes into play, we can cast this object to whatever we think it might be!
you can hover over this type in your IDE and see that types it CAN be, for example:
this is what it looks like in my IDE, tons of types! but don't let it scare you, we don't need all of them! in fact we only need a few!
reading the updates
alright lets start with something simple, we want to see all the new text messages!
we can do the following:
foreach (Update? update in updatesBase.UpdateList)
{
if (update is UpdateNewMessage newMessage)
{
}
}
in this code, we are Safe casting the update
to the type UpdateNewMessage
which is responsible for the incoming & outgoing new messages inside a (basic) group or private chat.
with the if statement
we indicate that if the update
is not what we wanted, do not PANIC, don't throw any exceptions, just relax and continue to the next update, if we did something like this:
foreach (Update? update in updatesBase.UpdateList)
{
var newMsg = (UpdateNewMessage)update;
}
it would cause some problems, since we are hard casting here, compiler would assume that it is 100% the given type, so it wont check for anything first, but we are not 100% sure that the update is UpdateNewMessage
, are we? it could be one of those many types that we saw earlier, so it will throw error and potentially break our application.
so its much cleaner and safer to do the pattern matching with the if statement ( or even switch-statements if you have multiple types to handle).
lets see what we have inside the newMessage
:
MessageBase message;
int pts;
int pts_count;
ignore the pts stuff here
we are seeing a new type: MessageBase
, you are going to see a lot of ..Base
types while working with Telegram API, (average OOP moment 😂)
but this one is not completely useless to us, it contains some information about the message, the 2 important ones are:
Peer Peer;
DateTime Date;
the Peer
is itself another long discussion but for now, lets say its a Person
for the sake of simplicity, this Peer
only has one field: ID
of the person(a unique identifier).
and the date
is the time when the message was sent.
so we can print them:
if (update is UpdateNewMessage newMessage)
{
var fromId = newMessage.message.Peer.ID;
Console.WriteLine($"msg from {fromId} at {newMessage.message.Date}");
}
you will see something like this in the console:
msg from 5298595170 at 1/7/2024 6:56:44 AM
so we got the data and the sender id
, but where the heck is the text?
not that fast, there is another type
the newMessage.message
can be one of these 3 things:
-
MessageEmpty
-
Message
-
MessageService
for now, we will be using Message
, so we can write:
if (update is UpdateNewMessage newMessage)
{
var fromId = newMessage.message.Peer.ID;
Console.WriteLine($"msg from {fromId} at {newMessage.message.Date}");
if (newMessage.message is Message msg)
{
}
}
so now the msg
contains the information we need, but first lets make this code a bit clear and more readable, we can return early when the update is something that we don't need:
if (update is not UpdateNewMessage newMessage)
continue;
var fromId = newMessage.message.Peer.ID;
Console.Write($"msg from {fromId} at {newMessage.message.Date}");
if (newMessage.message is not Message msg)
continue;
Console.WriteLine($" text: {msg.message}");
using the not
operator we indicate that if the update
is not what we wanted, just continue iterating over the list, and the same thing for the newMessage.message
and then finally we can print out all the text messages!
msg from 1548097770 at 1/7/2024 8:44:50 AM text: HELLO WTC!
The alternative
now that you've learned the update handling in the hard way, let me introduce you to the easy way:
there is an extension package called WTelegramClient.Extensions.Updates
, go ahead and install it:
> dotnet add package WTelegramClient.Extensions.Updates
using this package you can simply do this:
client.RegisterUpdateType<UpdateNewMessage>(async (message, updateBase) =>
{
Console.WriteLine($"msg from {message.message.Peer.ID}");
});
//or..
client.RegisterUpdateType<UpdateNewMessage>(OnNewMessage);
async Task OnNewMessage(UpdateNewMessage message, UpdatesBase? updateBase)
{
Console.WriteLine($"msg from {message.message.Peer.ID}");
}
so you can basically filter any Update
type that you want, there are other filters for Chat
types and more features, I recommend check it out:
WTelegramClient.Extensions.Updates
different types of content
so far we've learned how to print out the text messages, but there are other things to capture (e.x Documents, Photos, Videos..etc).
we'll cover a few of them in the following section.
Photos
first we want to put all the new pictures inside a Directory, lets call it photos
, so we should create it first:
//beginning
var photoDirectory = Path.Combine(Environment.CurrentDirectory, "photos");
if (!Directory.Exists(photoDirectory))
Directory.CreateDirectory(photoDirectory);
// ..login
//..update handler
simply check if the directory doesn't exist, create it!
now we can look for the photos in our UpdateHandler
:
if (newMessage.message is not Message msg)
continue;
if (msg.media is MessageMediaPhoto messageMediaPhoto)
{
}
okay so the msg.media
is a MessageMedia
which is just another Base
class, this one can be a lot of things as well. but now we only want the photos so we casted it to MessageMediaPhoto
.
the messageMediaPhoto
has a field called photo
which is yet another Base
class.
the photo
field can be 2 things:
PhotoEmpty
Photo
the first one is completely useless, in almost all the cases you want the Photo
, so instead of nesting if statements
we can write it like this:
if (msg.media is MessageMediaPhoto {photo: Photo thePicture}) { }
we are simply checking if the type is MessageMediaPhoto
AND its photo
field is of type Photo
in a single if-statement
and finally we are accessing it through the thePicture
variable.
keep in mind that if the msg.media
is not the specified type, the {photo: Photo thePicture}
part is not going to be executed, so you wont be able to use thePicture
.
this is equivalent to this code:
if (msg.media is MessageMediaPhoto messageMediaPhoto)
{
if (messageMediaPhoto.photo is Photo photo) { }
}
we are just checking the 2 conditions at the same level, you can also write it like this:
if (msg.media is MessageMediaPhoto messageMediaPhoto
&& messageMediaPhoto.photo is Photo photo) {}
thePicture
has everything you need to know about a picture, lets download it in our desired Directory.
if (msg.media is MessageMediaPhoto {photo: Photo thePicture})
{
var path = Path.Combine(photoDirectory, $"{thePicture.id}.jpg");
await using var fs = new FileStream(path, FileMode.OpenOrCreate);
await client.DownloadFileAsync(thePicture, fs);
}
not much to explain here, we are creating a path using the photo id (you can name it whatever you want) and we are creating a FileStream
using that path and using the DownloadFileAsync
helper method we are downloading it.
you can test this and go to the ../bin/Debug/net/photos
and see the photos you've downloaded!
Documents
in telegram terms, documents are basically any type of media except photo. so videos, GIFs, audio, voice, stickers ...etc.
so process is very similar to downloading photos, first create the directory:
var docDirectory = Path.Combine(Environment.CurrentDirectory, "documents");
if (!Directory.Exists(docDirectory))
Directory.CreateDirectory(docDirectory);
then check for the document type:
if (is photo...)
{
//..
}
else if (msg.media is MessageMediaDocument { document: Document doc }) //either Document or DocumentEmpty, same as PhotoBase
{
var filename = doc.Filename ?? $"{doc.id}.{doc.mime_type.Split('/')[1]}";
var path = Path.Combine(docDirectory, filename);
await using var fs = new FileStream(path, FileMode.OpenOrCreate);
await client.DownloadFileAsync(doc, fs);
}
so what we did here is a bit different, for the file name we can check if the FileName
has a value (??
) if so, we don't need to check for its type
, if it doesn't have any name, we split the mime_type
by /
mime types are represented like this
Video/mp4
orPhoto/jpg
..etc
and then we get the second index of the Split
so that we can only get the type
and with that we create a file name, then make a FileStream
and download the file. exactly the same as Photo, you can also go ahead and store the different types in separate directories based on this mime_type
but we are not going to do that here.
Blocking
you we've made it, we handled our updates, lets run the application;
what happened? why did the problem aborted immediately? well because we don't block
our thread.
after registering our handler:
client.OnUpdate += UpdateHandler;
there is basically no logic to tell the program: "wait until we tell you to exit" or something like that.
how can we fix it?
the most simplest solution could be this:
client.OnUpdate += UpdateHandler;
while (true)
await Task.Delay(TimeSpan.FromSeconds(1));
yes, put an infinite loop after registering the handler and just sleep for some amount of seconds, doesn't matter you can put TimeSpan.FromDays(99999)
, just the enough amount to not put any pressure on the CPU.
but this is not a perfect solution!
for a long term running or a production grade application I personally would recommend something like a Background Service (which we are going to cover in the next articles).
for testing, I think anything that works, is fine.
Conclusion
so we managed to handle basic updates that are coming into our account, we didn't do much, but we did the hard part, after this its going to be much more easier.
if you did not find what you were looking for here, you might want to take a look at these links:
if you still didn't find it, don't worry, leave a comment and tell us what could be the topic of the next article!
all the code written in this article is available here.
Posted on January 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.