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}