Merge pull request #198 from teloxide/bare_id

Expose bare chat id
This commit is contained in:
Waffle Maybe 2022-04-13 14:00:57 +04:00 committed by GitHub
commit b294631121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 50 deletions

View file

@ -22,8 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Me::username` and `Deref<Target = User>` implementation for `Me` ([#197][pr197])
- `Me::{mention, tme_url}` ([#197][pr197])
- `AllowedUpdate::ChatJoinRequest` ([#201][pr201])
- `ChatId::{is_user, is_group, is_channel_or_supergroup}` functions [#198][pr198]
[pr197]: https://github.com/teloxide/teloxide-core/pull/197
[pr198]: https://github.com/teloxide/teloxide-core/pull/198
[pr201]: https://github.com/teloxide/teloxide-core/pull/201
### Changed
@ -34,6 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Retry requests that previously returned `RetryAfter(_)` error
- `RequestError::RetryAfter` now has a `Duration` field instead of `i32`
### Fixed
- A bug in `Message::url` implementation ([#198][pr198])
## 0.4.5 - 2022-04-03
### Fixed

View file

@ -200,7 +200,7 @@ enum ChatIdHash {
impl ChatIdHash {
fn is_channel(&self) -> bool {
match self {
&Self::Id(id) => id.is_channel(),
&Self::Id(id) => id.is_channel_or_supergroup(),
Self::ChannelUsernameHash(_) => true,
}
}

View file

@ -12,27 +12,46 @@ use crate::types::UserId;
#[serde(transparent)]
pub struct ChatId(pub i64);
impl From<UserId> for ChatId {
fn from(UserId(id): UserId) -> Self {
Self(id as _)
}
/// Bare chat id as represented in MTProto API.
///
/// In MTProto API peer ids can have different types, for example `User(1)` and
/// `Group(1)` are different chats. For bot API these peer ids are encoded in
/// such a way that they can be stored in a simple integer (ie bot API chat ids
/// have the type encoded in them). This type exposes the "bare" "peer id" of a
/// chat.
///
/// `BareChatId` can be created by [`ChatId::to_bare`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum BareChatId {
User(UserId),
Group(u64),
/// Note: supergroups are considered channels.
Channel(u64),
}
impl ChatId {
pub(crate) fn is_channel(self) -> bool {
matches!(self.unmark(), UnmarkedChatId::Channel(_))
/// Returns `true` if this is an id of a user.
pub fn is_user(self) -> bool {
matches!(self.to_bare(), BareChatId::User(_))
}
pub(crate) fn unmark(self) -> UnmarkedChatId {
use UnmarkedChatId::*;
/// Returns `true` if this is an id of a group.
///
/// Note: supergroup is **not** considered a group.
pub fn is_group(self) -> bool {
matches!(self.to_bare(), BareChatId::Group(_))
}
// https://github.com/mtcute/mtcute/blob/6933ecc3f82dd2e9100f52b0afec128af564713b/packages/core/src/utils/peer-utils.ts#L4
const MIN_MARKED_CHANNEL_ID: i64 = -1997852516352;
const MAX_MARKED_CHANNEL_ID: i64 = -1000000000000;
const MIN_MARKED_CHAT_ID: i64 = MAX_MARKED_CHANNEL_ID + 1;
const MAX_MARKED_CHAT_ID: i64 = MIN_USER_ID - 1;
const MIN_USER_ID: i64 = 0;
const MAX_USER_ID: i64 = (1 << 40) - 1;
/// Returns `true` if this is an id of a channel.
pub fn is_channel_or_supergroup(self) -> bool {
matches!(self.to_bare(), BareChatId::Channel(_))
}
/// Converts this id to "bare" MTProto peer id.
///
/// See [`BareChatId`] for more.
pub(crate) fn to_bare(self) -> BareChatId {
use BareChatId::*;
match self.0 {
id @ MIN_MARKED_CHAT_ID..=MAX_MARKED_CHAT_ID => Group(-id as _),
@ -45,17 +64,39 @@ impl ChatId {
}
}
pub(crate) enum UnmarkedChatId {
User(UserId),
Group(u64),
Channel(u64),
impl From<UserId> for ChatId {
fn from(UserId(id): UserId) -> Self {
Self(id as _)
}
}
impl BareChatId {
/// Converts bare chat id back to normal bot API [`ChatId`].
#[allow(unused)]
pub(crate) fn to_bot_api(self) -> ChatId {
use BareChatId::*;
match self {
User(UserId(id)) => ChatId(id as _),
Group(id) => ChatId(-(id as i64)),
Channel(id) => ChatId(MAX_MARKED_CHANNEL_ID - (id as i64)),
}
}
}
// https://github.com/mtcute/mtcute/blob/6933ecc3f82dd2e9100f52b0afec128af564713b/packages/core/src/utils/peer-utils.ts#L4
const MIN_MARKED_CHANNEL_ID: i64 = -1997852516352;
const MAX_MARKED_CHANNEL_ID: i64 = -1000000000000;
const MIN_MARKED_CHAT_ID: i64 = MAX_MARKED_CHANNEL_ID + 1;
const MAX_MARKED_CHAT_ID: i64 = MIN_USER_ID - 1;
const MIN_USER_ID: i64 = 0;
const MAX_USER_ID: i64 = (1 << 40) - 1;
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use crate::types::{ChatId, UnmarkedChatId, UserId};
use crate::types::{BareChatId, ChatId, UserId};
/// Test that `ChatId` is serialized as the underlying integer
#[test]
@ -75,10 +116,44 @@ mod tests {
}
#[test]
fn user_id_unmark() {
fn chonky_user_id_to_bare() {
assert!(matches!(
ChatId(5298363099).unmark(),
UnmarkedChatId::User(UserId(5298363099))
ChatId(5298363099).to_bare(),
BareChatId::User(UserId(5298363099))
));
}
#[test]
fn to_bare_to_bot_api_identity() {
fn assert_identity(x: u64) {
use BareChatId::*;
assert_eq!(User(UserId(x)), User(UserId(x)).to_bot_api().to_bare());
assert_eq!(Group(x), Group(x).to_bot_api().to_bare());
assert_eq!(Channel(x), Channel(x).to_bot_api().to_bare());
}
// Somewhat random numbers
let ids = [
1,
4,
17,
34,
51,
777000,
1000000,
617136926,
1666111087,
1 << 20,
(1 << 35) | 123456,
];
// rust 2021 when :(
ids.iter().copied().for_each(assert_identity);
}
#[test]
fn display() {
assert_eq!(ChatId(1).to_string(), "1");
}
}

View file

@ -4,10 +4,11 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::{
Animation, Audio, Chat, ChatId, Contact, Dice, Document, Game, InlineKeyboardMarkup, Invoice,
Location, MessageAutoDeleteTimerChanged, MessageEntity, PassportData, PhotoSize, Poll,
ProximityAlertTriggered, Sticker, SuccessfulPayment, True, User, Venue, Video, VideoNote,
Voice, VoiceChatEnded, VoiceChatParticipantsInvited, VoiceChatScheduled, VoiceChatStarted,
Animation, Audio, BareChatId, Chat, ChatId, Contact, Dice, Document, Game,
InlineKeyboardMarkup, Invoice, Location, MessageAutoDeleteTimerChanged, MessageEntity,
PassportData, PhotoSize, Poll, ProximityAlertTriggered, Sticker, SuccessfulPayment, True, User,
Venue, Video, VideoNote, Voice, VoiceChatEnded, VoiceChatParticipantsInvited,
VoiceChatScheduled, VoiceChatStarted,
};
/// This object represents a message.
@ -1050,30 +1051,35 @@ impl Message {
/// Note that for private groups the link will only be accessible for group
/// members.
///
/// Returns `None` for private chats (i.e.: DMs).
/// Returns `None` for private chats (i.e.: DMs) and private groups (not
/// supergroups).
pub fn url(&self) -> Option<reqwest::Url> {
if self.chat.is_private() {
use BareChatId::*;
// Note: `t.me` links use bare chat ids
let chat_id = match self.chat.id.to_bare() {
// For private chats (i.e.: DMs) we can't produce "normal" t.me link.
//
// There are "tg://openmessage?user_id={0}&message_id={1}" links, which are
// supposed to open any chat, including private messages, but they
// are only supported by some telegram clients (e.g. Plus Messenger,
// Telegram for Android 4.9+).
return None;
}
User(_) => return None,
// Similarly to user chats, there is no way to create a link to a message in a normal,
// private group.
//
// (public groups are always supergroup which are in turn channels).
Group(_) => return None,
Channel(id) => id,
};
let url = match self.chat.username() {
// If it's public group (i.e. not DM, not private group), we can produce
// "normal" t.me link (accessible to everyone).
Some(username) => format!("https://t.me/{0}/{1}/", username, self.id),
// For private groups we produce "private" t.me/c links. These are only
// accessible to the group members.
None => format!(
"https://t.me/c/{0}/{1}/",
// FIXME: this may be wrong for private channels
(-self.chat.id.0) - 1000000000000,
self.id
),
// For private supergroups and channels we produce "private" t.me/c links. These are
// only accessible to the group members.
None => format!("https://t.me/c/{0}/{1}/", chat_id, self.id),
};
// UNWRAP:

View file

@ -19,16 +19,6 @@ pub enum Recipient {
ChannelUsername(String),
}
impl Recipient {
#[allow(unused)]
pub(crate) fn is_channel(&self) -> bool {
match self {
Recipient::Id(id) => id.is_channel(),
Recipient::ChannelUsername(_) => true,
}
}
}
impl From<UserId> for Recipient {
fn from(id: UserId) -> Self {
Self::Id(id.into())