Peers
One of the most important concepts in MTProto is the "peer". Peer is an object that defines a user, a chat or a channel, and is widely used within the APIs.
What is a Peer?
In MTProto, there are 2 types representing a peer: Peer and InputPeer
Peer defines the peer type (user/chat/channel) and its ID, and is usually returned by the server inside some other object (like Message).
InputPeer defines the peer by providing its type, ID and access hash. Access hashes are a mechanism designed to prevent users from accessing peers that they never met. These objects are mostly used when sending RPC queries to Telegram.
There are also
InputUser
andInputChannel
that are used to prevent clients from passing incorrect peers (e.g. restricting a user in a legacy group chat).They are basically the same, and you should only care about them when using Raw APIs, so we'll skip them for now.
TIP
In MTProto, you'll often see InputSomething
and Something
types.
This simply means that the former should be used when making requests, and the latter is sent by the server back.
Chats and Channels
As you may have noticed, in MTProto there are only three types of peers: users, chats and channels. However, things are not as simple as you may imagine, so let's dive a bit deeper.
Chat is a legacy group. The one which is created by default when you use "Create group" button in official clients. Official clients refer to them as "Groups"
Channel is anything that is not a user, nor a legacy group. Supergroups, actual broadcast channels and broadcast groups are all represented in MTProto as a Channel with a different set of flags:
- A broadcast channel is a Channel where
.broadcast === true
- A supergroup (also referred to as megagroup) is a Channel where
.megagroup === true
- A forum is a supergroup where
.forum === true
- A broadcast group (also referred to as gigagroup) is a Channel where
.gigagroup === true
. They are basically a supergroup where default permissions disallow sending messages and cannot be changed (src).
Official clients only use "Channel" when referring to broadcast channels.
Chats are still used (probably?) because they are enough for most people's needs, and are also lighter on server resources. However, chats are missing many important features for public communities, like: usernames, custom admin rights, per-user restrictions, event log and more.
Official clients silently "migrate" legacy groups to supergroups (actually channels) whenever the user wants to use a feature not supported by the Chat, like setting a username. Channel cannot be migrated back to Chat.
Chat in mtcute
In addition to the mess described above, mtcute also has a Chat type. It is used to represent anything where there can be messages, including users, legacy groups, supergroups, channels, etc.
It is mostly inspired by Bot API's Chat type, and works in a very similar way.
Fetching peers
Often, you'll need to interact with a peer later, when the respective User
or Chat
object is no longer available.
Of course, manually storing peers along with their access hash is very tedious. That is why mtcute handles it for you! Peers and their access hashes are automatically stored inside the storage you provided, and can be accessed at any time later.
This way, to get an InputPeer, you simply need to call resolvePeer
method and provide a Peer
const peer = await tg.resolvePeer({ _: 'peerUser', userId: 777000 })
const peer = await tg.resolvePeer({ _: 'peerUser', userId: 777000 })
But still, this is very tedious, so you can pass many more than just a Peer
object to resolvePeer
:
- Peer's marked ID
- Peer's username
- Peer's phone number (will only work with contacts)
"me"
or"self"
to refer to yourself (current user)Peer
,InputPeer
,InputUser
andInputChannel
objectsChat
,User
or generally anything where.inputPeer: InputPeer
is available
const peer = await tg.resolvePeer(-1001234567890)
const peer = await tg.resolvePeer("durovschat")
const peer = await tg.resolvePeer("+79001234567")
const peer = await tg.resolvePeer("me")
const peer = await tg.resolvePeer(-1001234567890)
const peer = await tg.resolvePeer("durovschat")
const peer = await tg.resolvePeer("+79001234567")
const peer = await tg.resolvePeer("me")
InputPeerLike
However, you will only really need to use resolvePeer
method when you are working with the Raw APIs. High-level methods use InputPeerLike
, a special type used to represent an input peer.
In fact, we have already covered it - it is the very type that resolvePeer
takes as its argument:
async function resolvePeer(peer: InputPeerLike): Promise<tl.TypeInputPeer>
async function resolvePeer(peer: InputPeerLike): Promise<tl.TypeInputPeer>
Client methods implicitly call resolvePeer
to convert InputPeerLike
to input peer and use it for the MTProto API call.
TIP
Whenever possible, pass Chat
, InputPeer
or "me"/"self"
as InputPeerLike
, since this avoids redundant storage and/or API calls.
When not possible, use their marked IDs, since most of the time it is the cheapest way to fetch an InputPeer
.
For smaller-scale scripts, you can use usernames and phone numbers. They are also cached in the storage, but might require additional API call if they are not. Also, not every user has a username, and only your contacts can be fetched by the phone number.
// ❌ BAD
await tg.sendText(msg.sender.username!, ...)
await tg.sendText(msg.sender.phone!, ...)
// 🧐 BETTER
await tg.sendText(msg.sender.id, ...)
// ✅ GOOD
await tg.sendText(msg.sender.inputPeer, ...)
await tg.sendText(msg.sender, ...)
// ❌ BAD
await tg.sendText(msg.sender.username!, ...)
await tg.sendText(msg.sender.phone!, ...)
// 🧐 BETTER
await tg.sendText(msg.sender.id, ...)
// ✅ GOOD
await tg.sendText(msg.sender.inputPeer, ...)
await tg.sendText(msg.sender, ...)
Marked IDs
As you may have noticed, both Peer
and InputPeer
contain peer type, but when using client methods and Bot API, you don't specify it manually.
Peer
and InputPeer
contain what is called as a "bare" ID, the ID inside that particular peer type. Bare IDs between different peer types may (and do!) collide, and that is why Marked IDs are used.
Marked ID is a slightly transformed variant of the bare ID to unambiguously define both peer type and peer ID with a single integer (currently, it fits into JS number, but later we may be forced to move to 64-bit integers).
This was first introduced in TDLib, and was since adopted by many third-party libraries, including mtcute.
TIP
The concept described below is implemented and exported in utils, see getBasicPeerType, getMarkedPeerId,
It works as follows:
User
IDs are kept as-is (123 -> 123
)Chat
IDs are negated (123 -> -123
)Channel
IDs are subtracted from-1000000000000
(1234567890 -> -1001234567890
)Some sources may say that it's simply prepending
-100
to the ID, but that's not entirely true, since channel ID may be not 10 digits long.
This way, you can easily determine peer type:
const MIN_CHANNEL_ID = -1002147483647
const MAX_CHANNEL_ID = -1000000000000
const MIN_CHAT_ID = -2147483647
const MAX_USER_ID = 2147483647
if (peer < 0) {
if (MIN_CHAT_ID <= peer) return 'chat'
if (MIN_CHANNEL_ID <= peer && peer < MAX_CHANNEL_ID) return 'channel'
} else if (0 < peer && peer <= MAX_USER_ID) {
return 'user'
}
const MIN_CHANNEL_ID = -1002147483647
const MAX_CHANNEL_ID = -1000000000000
const MIN_CHAT_ID = -2147483647
const MAX_USER_ID = 2147483647
if (peer < 0) {
if (MIN_CHAT_ID <= peer) return 'chat'
if (MIN_CHANNEL_ID <= peer && peer < MAX_CHANNEL_ID) return 'channel'
} else if (0 < peer && peer <= MAX_USER_ID) {
return 'user'
}
And then, to convert it back to bare ID, use the exact same operation (it works in two directions).
Incomplete peers
In some cases, mtcute may not be able to immediately provide you with complete information about a user/chat (see min constructors in MTProto docs).
This currently only seems to happen for msg.sender
and msg.chat
fields for non-bot accounts in large chats, so if you're only ever going to work with bots, you can safely ignore this section (for now?).
TIP
Complete peers ≠ full peers!
- Incomplete are seen in updates in rare cases, and are missing some fields (e.g. username)
- Complete peers are pretty much all the other peer objects you get in updates
- Full peers are objects with additional information (e.g. bio) which you should request explicitly
For such chats, the server may send "min" constructors, which contain incomplete information about the user/chat. To avoid blocking the updates loop, and since the missing information is not critical, mtcute will return an incomplete peer object, which is good enough for most cases.
For example, such User
objects may have the following data missing or have incorrect values:
.username
may be missing.photo
may be missing with some privacy settings- online status may be incorrect
- and probably more (for more info please consult the official docs for user and channel)
The user itself is still usable, though. If you need to get the missing information, you can call getUsers
/getChat
method, which will return a complete User
/Chat
object.
const user = ... // incomplete user from somewhere
const [completeUser] = await tg.getUsers(user)
const user = ... // incomplete user from somewhere
const [completeUser] = await tg.getUsers(user)
If you are using Dispatcher, you can use .getCompleteSender()
or .getCompleteChat()
methods instead:
dp.onNewMessage(async (msg) => {
const sender = await msg.getCompleteSender()
const chat = await msg.getCompleteChat()
})
dp.onNewMessage(async (msg) => {
const sender = await msg.getCompleteSender()
const chat = await msg.getCompleteChat()
})
Or you can use withCompleteSender
and withCompleteChat
middleware-like filters:
dp.onNewMessage(
filters.withCompleteSender(filters.sender('user'))
async (msg) => {
const user = msg.sender
// user is guaranteed to be complete
}
)
dp.onNewMessage(
filters.withCompleteSender(filters.sender('user'))
async (msg) => {
const user = msg.sender
// user is guaranteed to be complete
}
)