egg_mode/direct/
raw.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use crate::common::*;
6
7use std::collections::HashMap;
8
9use chrono;
10use serde::Deserialize;
11
12use crate::entities::MediaEntity;
13use crate::tweet::TweetSource;
14
15use super::{Cta, DMEntities, DirectMessage, QuickReply};
16
17// n.b. all of the types in this module are re-exported in `raw::types::direct` - these docs are
18// public!
19
20/// Minimally-processed form of `DirectMessage`, prior to changing byte indices or loading
21/// source-app information.
22///
23/// The `RawDirectMessage` type is used in the process of converting from `EventCursor` or
24/// `SingleEvent` into a `DirectMessage`. They can be directly loaded from a `DMEvent` struct, but
25/// require a mapping of source-app IDs to convert fully into a `DirectMessage`. By giving this
26/// mapping to the `into_dm` function, you can convert a `RawDirectMessage` into the final
27/// `DirectMessage` type.
28///
29/// Another way `RawDirectMessage` differs from `DirectMessage` is how its entities are stored.
30/// Twitter returns entity information based on *codepoint* indices, whereas Rust Strings are
31/// indexed using *byte* indices. egg-mode translates these indices for you when returning a
32/// processed type, but that translation has not occurred when a `RawDirectMessage` has been
33/// created. The `translate_indices` function can be used to perform this translation if the
34/// `RawDirectMessage` is being used directly. The `into_dm` conversion function also performs this
35/// translation before returning the final `DirectMessage`.
36#[derive(Debug, Deserialize)]
37#[serde(from = "DMEvent")]
38pub struct RawDirectMessage {
39    /// Numeric ID for this DM.
40    pub id: u64,
41    /// UTC timestamp from when this DM was created.
42    pub created_at: chrono::DateTime<chrono::Utc>,
43    /// The text of the DM.
44    pub text: String,
45    /// Link, hashtag, and user mention information parsed out of the DM.
46    pub entities: DMEntities,
47    /// Media attached to the DM, if present.
48    pub attachment: Option<MediaEntity>,
49    /// A list of "call to action" buttons, if present.
50    pub ctas: Option<Vec<Cta>>,
51    /// A list of "quick reply" options, if present.
52    pub quick_replies: Option<Vec<QuickReply>>,
53    /// The `metadata` associated with the Quick Reply chosen by the sender, if present.
54    pub quick_reply_response: Option<String>,
55    /// The ID of the user who sent the DM.
56    pub sender_id: u64,
57    /// The string ID associated with the app used to send the DM, if sent by the authenticated
58    /// user.
59    pub source_app_id: Option<String>,
60    /// The ID of the user who received the DM.
61    pub recipient_id: u64,
62    translated: bool,
63}
64
65impl RawDirectMessage {
66    /// Translates the codepoint-based indices in this `RawDirectMessage`'s entities into
67    /// byte-based ones.
68    ///
69    /// Note that `into_dm` also performs this conversion, so if you're ultimately planning to
70    /// convert this into a `DirectMessage`, you shouldn't need to call this function directly.
71    /// `RawDirectMessage` tracks whether this translation has occured, so if you need to access
72    /// the fields before converting, the final conversion won't double-translate and leave you
73    /// with invalid indices.
74    pub fn translate_indices(&mut self) {
75        if !self.translated {
76            self.translated = true;
77
78            for entity in &mut self.entities.hashtags {
79                codepoints_to_bytes(&mut entity.range, &self.text);
80            }
81            for entity in &mut self.entities.symbols {
82                codepoints_to_bytes(&mut entity.range, &self.text);
83            }
84            for entity in &mut self.entities.urls {
85                codepoints_to_bytes(&mut entity.range, &self.text);
86            }
87            for entity in &mut self.entities.user_mentions {
88                codepoints_to_bytes(&mut entity.range, &self.text);
89            }
90            if let Some(ref mut media) = self.attachment {
91                codepoints_to_bytes(&mut media.range, &self.text);
92            }
93        }
94    }
95
96    /// Converts this `RawDirectMessage` into a `DirectMessage`, using the given source-app
97    /// mapping.
98    ///
99    /// If the ID given in `source_app` is not present in the `apps` mapping, the source-app
100    /// information is discarded.
101    ///
102    /// This conversion also calls `translate_indices` before constructing the `DirectMessage`.
103    pub fn into_dm(mut self, apps: &HashMap<String, TweetSource>) -> DirectMessage {
104        self.translate_indices();
105        let source_app = self.source_app_id.and_then(|id| apps.get(&id).cloned());
106
107        DirectMessage {
108            id: self.id,
109            created_at: self.created_at,
110            text: self.text,
111            entities: self.entities,
112            attachment: self.attachment,
113            ctas: self.ctas,
114            sender_id: self.sender_id,
115            source_app,
116            recipient_id: self.recipient_id,
117            quick_replies: self.quick_replies,
118            quick_reply_response: self.quick_reply_response,
119        }
120    }
121
122    // TODO: provide a conversion that drops source-app information?
123}
124
125// DMs received from twitter are structured as events in their activity API, which means they have
126// a lot of deep nesting for how they are structured. The types and From impl below convert that
127// into a flat object ready for processing/export by egg-mode.
128
129impl From<DMEvent> for RawDirectMessage {
130    fn from(ev: DMEvent) -> RawDirectMessage {
131        use chrono::TimeZone;
132        RawDirectMessage {
133            id: ev.id,
134            created_at: chrono::Utc.timestamp_millis(ev.created_timestamp),
135            text: ev.message_create.message_data.text,
136            entities: ev.message_create.message_data.entities,
137            attachment: ev.message_create.message_data.attachment.map(|a| a.media),
138            ctas: ev.message_create.message_data.ctas,
139            sender_id: ev.message_create.sender_id,
140            source_app_id: ev.message_create.source_app_id,
141            recipient_id: ev.message_create.target.recipient_id,
142            quick_replies: ev
143                .message_create
144                .message_data
145                .quick_reply
146                .map(|q| q.options),
147            quick_reply_response: ev
148                .message_create
149                .message_data
150                .quick_reply_response
151                .map(|q| q.metadata),
152            translated: false,
153        }
154    }
155}
156
157/// Single direct message event.
158#[derive(Deserialize)]
159pub struct SingleEvent {
160    /// Information about the event.
161    pub event: EventType,
162    /// Mapping of source app ID to information about the app, if this message was sent by the
163    /// authenticated user.
164    #[serde(default)]
165    pub apps: HashMap<String, TweetSource>,
166}
167
168/// Listing of direct message events, represented as a cursored page within a larger data set.
169#[derive(Deserialize)]
170pub struct EventCursor {
171    /// The list of events contained on this page.
172    pub events: Vec<EventType>,
173    /// The mapping of source app IDs to information about the app, if messages on this page were
174    /// sent by the authenticated user.
175    #[serde(default)]
176    pub apps: HashMap<String, TweetSource>,
177    /// String ID for the next page of message events, if more exist.
178    pub next_cursor: Option<String>,
179}
180
181/// Wrapper enum to represent a `DMEvent` in the Account Activity API.
182///
183/// As direct messages are part of the Account Activity API, they are presented as an event type in
184/// a broader event envelope. This enum mainly encapsulates the requirement that direct messages
185/// are returned as the `message_create` event type with the proper data structure.
186#[derive(Deserialize)]
187#[serde(tag = "type")]
188#[serde(rename_all = "snake_case")]
189pub enum EventType {
190    /// A `message_create` event, representing a direct message.
191    ///
192    /// The `message_create` event structure is flattened into a `RawDirectMessage` when
193    /// deserializing. It should be combined with the `apps` map in a `SingleEvent` or
194    /// `EventCursor` when converting into a `DirectMessage`.
195    MessageCreate(RawDirectMessage),
196}
197
198impl EventType {
199    /// Returns the inner `RawDirectMessage` structure from the `message_create` event.
200    pub fn as_raw_dm(self) -> RawDirectMessage {
201        let EventType::MessageCreate(dm) = self;
202        dm
203    }
204}
205
206/// The root `message_create` event, representing a direct message.
207#[derive(Deserialize)]
208struct DMEvent {
209    /// Numeric ID for the direct message.
210    #[serde(with = "serde_via_string")]
211    id: u64,
212    /// UTC Unix timestamp for when the message was sent, encoded as the number of milliseconds
213    /// since the Unix epoch.
214    #[serde(with = "serde_via_string")]
215    created_timestamp: i64,
216    /// Message data for this event.
217    message_create: MessageCreateEvent,
218}
219
220/// The `message_create` data of a `DMEvent`, containing information about the direct message.
221#[derive(Deserialize)]
222struct MessageCreateEvent {
223    /// The `message_data` portion of this event.
224    message_data: MessageData,
225    #[serde(with = "serde_via_string")]
226    /// The numeric User ID of the sender.
227    sender_id: u64,
228    /// The string ID of the app used to send the message, if it was sent by the authenticated
229    /// user.
230    source_app_id: Option<String>,
231    /// Information about the recipient of the message.
232    target: MessageTarget,
233}
234
235/// The `message_data` portion of a `DMEvent`, containing the bulk of information about a direct
236/// message.
237#[derive(Deserialize)]
238struct MessageData {
239    /// A list of "call to action" buttons, if present.
240    ctas: Option<Vec<Cta>>,
241    /// Information about attached media, if present.
242    attachment: Option<MessageAttachment>,
243    /// Information about URL, hashtag, or user-mention entities used in the message.
244    entities: DMEntities,
245    /// Information about Quick Reply options, if present.
246    quick_reply: Option<RawQuickReply>,
247    /// Information about a selected Quick Reply option, if the sender selected one.
248    quick_reply_response: Option<QuickReplyResponse>,
249    /// The message text.
250    text: String,
251}
252
253/// Represents attached media information from within a `DMEvent`.
254#[derive(Deserialize)]
255struct MessageAttachment {
256    /// Information about the attached media.
257    ///
258    /// Note that the indices used within the `MediaEntity` are received from Twitter using
259    /// codepoint-based indexing. Using the indices from within this type directly without
260    /// translating them may result in string-slicing errors or panics unless you translate the
261    /// indices or use `char_indices` and `enumerate` yourself to ensure proper use of the indices.
262    media: MediaEntity,
263}
264
265/// Represents a list of Quick Reply options from within a `DMEvent`.
266#[derive(Deserialize)]
267struct RawQuickReply {
268    /// The list of Quick Reply options sent with this message.
269    options: Vec<QuickReply>,
270}
271
272/// Represents the `metadata` from a selected Quick Reply from within a `DMEvent`.
273#[derive(Deserialize)]
274struct QuickReplyResponse {
275    /// The `metadata` field for the Quick Reply option the sender selected.
276    metadata: String,
277}
278
279/// Represents the message target from within a `DMEvent`.
280#[derive(Deserialize)]
281struct MessageTarget {
282    #[serde(with = "serde_via_string")]
283    /// The numeric user ID of the recipient of the message.
284    recipient_id: u64,
285}