Update handling in WTelegramClient

mrali109

Ali Salehi

Posted on January 9, 2024

Update handling in WTelegramClient

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Github

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;
}
Enter fullscreen mode Exit fullscreen mode

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 the async.

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;
Enter fullscreen mode Exit fullscreen mode

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)
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

take a closer look, the update variable is an Update?. what is it?

public abstract class Update : IObject { }
Enter fullscreen mode Exit fullscreen mode

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:

types
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)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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}");
}
Enter fullscreen mode Exit fullscreen mode

you will see something like this in the console:

msg from 5298595170 at 1/7/2024 6:56:44 AM
Enter fullscreen mode Exit fullscreen mode

so we got the data and the sender id, but where the heck is the text?

not that fast, there is another type

crying kid meme

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)
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

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}");
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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}");
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
{

}
Enter fullscreen mode Exit fullscreen mode

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}) { }
Enter fullscreen mode Exit fullscreen mode

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) { }
}
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 or Photo/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;
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
mrali109
Ali Salehi

Posted on January 9, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related