From 693121a6f6b3c56e3babf3270cde81c0339ccada Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 5 Jun 2023 15:38:35 +0400 Subject: [PATCH] Add `ThreadId` newtype --- crates/teloxide-core/CHANGELOG.md | 2 + crates/teloxide-core/src/types.rs | 2 + crates/teloxide-core/src/types/thread_id.rs | 80 +++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 crates/teloxide-core/src/types/thread_id.rs diff --git a/crates/teloxide-core/CHANGELOG.md b/crates/teloxide-core/CHANGELOG.md index aac0f5d8..e6008955 100644 --- a/crates/teloxide-core/CHANGELOG.md +++ b/crates/teloxide-core/CHANGELOG.md @@ -15,8 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Update::from`, a replacement for `Update::user` ([#850][pr850]) - `Seconds` type, which represents a duration is seconds ([#859][pr859]) - `VideoChatEnded::duration` field that was previously missed ([#859][pr859]) +- `ThreadId` newtype over `MessageId`, used for identifying reply threads ([#887][pr887]) [pr851]: https://github.com/teloxide/teloxide/pull/851 +[pr887]: https://github.com/teloxide/teloxide/pull/887 ### Fixed diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs index 27314302..96ae48ea 100644 --- a/crates/teloxide-core/src/types.rs +++ b/crates/teloxide-core/src/types.rs @@ -100,6 +100,7 @@ pub use sticker::*; pub use sticker_set::*; pub use successful_payment::*; pub use target_message::*; +pub use thread_id::*; pub use unit_false::*; pub use unit_true::*; pub use update::*; @@ -191,6 +192,7 @@ mod sticker; mod sticker_set; mod successful_payment; mod target_message; +mod thread_id; mod unit_false; mod unit_true; mod update; diff --git a/crates/teloxide-core/src/types/thread_id.rs b/crates/teloxide-core/src/types/thread_id.rs new file mode 100644 index 00000000..2e3f50ac --- /dev/null +++ b/crates/teloxide-core/src/types/thread_id.rs @@ -0,0 +1,80 @@ +use crate::types::MessageId; + +use serde::{Deserialize, Serialize}; + +/// Reply thread identifier. +/// +/// A message that isn't a reply and other messages that reply to it directly or +/// indirectly through a reply chain constitute a reply thread. All messages +/// except the initial root message have an additional [`thread_id`] field that +/// is equal to the root message's id. +/// +/// In other words a thread id can be found recursively following +/// `reply_to_message_id`, until you find a message which does not reply to +/// anything, it's id is the thread id. +/// +/// For example: +/// +/// ```text +/// lizard: Hi {id:17} <-------------+--------------+----+--------------+---------------------+ +/// | | | | | +/// wizard: hewwo {id:18, reply: 17 -+, thread: 17 -+} | | | +/// | | | +/// lizard: I've been wondering [...] {id:19, reply: 17 -+, thread: 17 -+} <---+ | +/// | | +/// neushoorn: wait, did y'all know that [...] {id:20} <-----------------------)--------------)--+-----+ +/// | | | | +/// wizard: so this is not an easy question, actually [...] {id:21, reply: 19 -+, thread: 17 -+} | | +/// +--------+ | +/// | | +/// wizard: everyone knows that, how did you not know that before?... {id:22, reply:20 -+, thread: 20 -+} +/// ``` +/// +/// Note that channel comments and forum topics, reuse threads for different +/// meanings. For channel comments every comment (indirectly) replies to the +/// channel post forwarded to the linked chat (i.e. the forwarded channel post +/// is the root of the comments thread). For forum topics every message in a +/// topic is in the same thread too (i.e. they (indirectly) reply to the start +/// of the topic). +/// +/// [`thread_id`]: crate::types::Message::thread_id +#[derive(Clone, Copy, Debug, derive_more::Display, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(from = "ThreadIdRaw", into = "ThreadIdRaw")] +pub struct ThreadId(/** Identifier of the root message in a reply thread. */ pub MessageId); + +// N.B. this is a hack to [de]serialize `ThreadId` as just a number +// we need this since `MessageId` is [de]serialized as `{"message_id":n}`. + +#[derive(Serialize, Deserialize)] +struct ThreadIdRaw(i32); + +impl From for ThreadId { + fn from(ThreadIdRaw(message_id): ThreadIdRaw) -> Self { + ThreadId(MessageId(message_id)) + } +} + +impl From for ThreadIdRaw { + fn from(ThreadId(MessageId(message_id)): ThreadId) -> Self { + ThreadIdRaw(message_id) + } +} + +#[cfg(test)] +mod tests { + use crate::types::{MessageId, ThreadId}; + + #[test] + fn smoke_deser() { + let json = r#"123"#; + let mid: ThreadId = serde_json::from_str(json).unwrap(); + assert_eq!(mid, ThreadId(MessageId(123))); + } + + #[test] + fn smoke_ser() { + let mid: ThreadId = ThreadId(MessageId(123)); + let json = serde_json::to_string(&mid).unwrap(); + assert_eq!(json, r#"123"#); + } +}