|
1 <?php |
|
2 |
|
3 class AKTT_Tweet { |
|
4 var $post_id = null; |
|
5 var $featured_image_id = null; |
|
6 var $raw_data = null; |
|
7 |
|
8 /** |
|
9 * Set up the tweet with the ID from twitter |
|
10 * |
|
11 * @param mixed $data - tweet id or full tweet object from twitter API |
|
12 * @param bool $from_db - Whether to auto-populate this object from the DB |
|
13 */ |
|
14 function __construct($data, $from_db = false) { |
|
15 if (is_object($data)) { |
|
16 $this->populate_from_twitter_obj($data); |
|
17 } |
|
18 else { |
|
19 // Assign the tweet ID to this object |
|
20 $this->id = $data; |
|
21 |
|
22 // Flag to populate the object from the DB on construct |
|
23 if ($from_db == true) { |
|
24 $this->populate_from_db(); |
|
25 } |
|
26 } |
|
27 } |
|
28 |
|
29 |
|
30 /** |
|
31 * Allows the object's properties to be populated from a post object in the DB |
|
32 * |
|
33 * @return void |
|
34 */ |
|
35 function populate_from_db() { |
|
36 $post = $this->get_post(AKTT::$post_type); |
|
37 |
|
38 if (is_wp_error($post) || empty($post)) { |
|
39 return false; |
|
40 } |
|
41 |
|
42 $this->post = $post; |
|
43 $this->raw_data = get_post_meta($this->post->ID, '_aktt_tweet_raw_data', true); |
|
44 $this->data = json_decode($this->raw_data); |
|
45 } |
|
46 |
|
47 |
|
48 /** |
|
49 * Populates the object from a twitter API response object |
|
50 * |
|
51 * @param object $tweet_obj |
|
52 * @return void |
|
53 */ |
|
54 function populate_from_twitter_obj($tweet_obj) { |
|
55 $this->data = $tweet_obj; |
|
56 $this->raw_data = json_encode($tweet_obj); |
|
57 $this->id = $tweet_obj->id_str; |
|
58 } |
|
59 |
|
60 /** |
|
61 * Accessor function for tweet id |
|
62 * |
|
63 * @return string|null |
|
64 */ |
|
65 public function id() { |
|
66 return (isset($this->data) ? $this->data->id_str : null); |
|
67 } |
|
68 |
|
69 /** |
|
70 * Accessor function for tweet text shortened for post title |
|
71 * |
|
72 * @return string |
|
73 */ |
|
74 public function title() { |
|
75 if (isset($this->data)) { |
|
76 $title = trim(AKTT::substr($this->data->text, 0, 50)); |
|
77 if (AKTT::strlen($this->data->text) > 50) { |
|
78 $title = $title.'...'; |
|
79 } |
|
80 } |
|
81 else { |
|
82 $title = null; |
|
83 } |
|
84 return $title; |
|
85 } |
|
86 |
|
87 /** |
|
88 * Accessor function for tweet text |
|
89 * |
|
90 * @return string |
|
91 */ |
|
92 public function content() { |
|
93 if (isset($this->data) && isset($this->data->text)) { |
|
94 return $this->data->text; |
|
95 } |
|
96 if (isset($this->post) && isset($this->post->post_content)) { |
|
97 return $this->post->post_content; |
|
98 } |
|
99 return null; |
|
100 } |
|
101 |
|
102 /** |
|
103 * Accessor function for tweet date/time |
|
104 * |
|
105 * @return string |
|
106 */ |
|
107 public function date() { |
|
108 return (isset($this->data) ? $this->data->created_at : null); |
|
109 } |
|
110 |
|
111 /** |
|
112 * Accessor function for tweet author's username |
|
113 * |
|
114 * @return string |
|
115 */ |
|
116 public function username() { |
|
117 return (isset($this->data) ? $this->data->user->screen_name : null); |
|
118 } |
|
119 |
|
120 /** |
|
121 * Accessor function for tweet reply-to username |
|
122 * |
|
123 * @return string |
|
124 */ |
|
125 public function reply_screen_name() { |
|
126 return (isset($this->data) ? $this->data->in_reply_to_screen_name : null); |
|
127 } |
|
128 |
|
129 /** |
|
130 * Accessor function for tweet reply-to tweet id |
|
131 * |
|
132 * @return string |
|
133 */ |
|
134 public function reply_id() { |
|
135 return (isset($this->data) ? $this->data->in_reply_to_status_id_str : null); |
|
136 } |
|
137 |
|
138 /** |
|
139 * Accessor function for tweet's hashtags |
|
140 * |
|
141 * @return string |
|
142 */ |
|
143 public function hashtags() { |
|
144 return (isset($this->data) && isset($this->data->entities) ? $this->data->entities->hashtags : array()); |
|
145 } |
|
146 |
|
147 /** |
|
148 * Accessor function for tweet's mentions |
|
149 * |
|
150 * @return string |
|
151 */ |
|
152 public function mentions() { |
|
153 return (isset($this->data) && isset($this->data->entities) ? $this->data->entities->user_mentions : array()); |
|
154 } |
|
155 |
|
156 /** |
|
157 * Accessor function for tweet's URLS |
|
158 * |
|
159 * @return string |
|
160 */ |
|
161 public function urls() { |
|
162 return (isset($this->data) && isset($this->data->entities) ? $this->data->entities->urls : array()); |
|
163 } |
|
164 |
|
165 /** |
|
166 * Accessor function for tweet's status URL on Twitter |
|
167 * |
|
168 * @return string |
|
169 */ |
|
170 public function status_url() { |
|
171 if ($username = $this->username() && $id = $this->id()) { |
|
172 return AKTT::status_url($username, $id); |
|
173 } |
|
174 return null; |
|
175 } |
|
176 |
|
177 /** |
|
178 * Takes the twitter date format and gets a timestamp from it |
|
179 * |
|
180 * @param string $date - "Fri Aug 05 20:33:38 +0000 2011" |
|
181 * @return int - timestamp |
|
182 */ |
|
183 static function twdate_to_time($date) { |
|
184 $parts = explode(' ', $date); |
|
185 return strtotime($parts[1].' '.$parts[2].', '.$parts[5].' '.$parts[3]); |
|
186 } |
|
187 |
|
188 |
|
189 /** |
|
190 * See if the tweet ID matches any tweet ID post meta value |
|
191 * |
|
192 * @return bool |
|
193 */ |
|
194 function exists() { |
|
195 $test = $this->get_post(AKTT::$post_type); |
|
196 return (bool) (count($test) > 0); |
|
197 } |
|
198 |
|
199 function exists_by_guid() { |
|
200 global $wpdb; |
|
201 $guid = $this->guid(); |
|
202 if (empty($guid)) { |
|
203 return false; |
|
204 } |
|
205 $count = $wpdb->get_var($wpdb->prepare(" |
|
206 SELECT COUNT(ID) |
|
207 FROM $wpdb->posts |
|
208 WHERE guid = %s |
|
209 ", $guid)); |
|
210 return (bool) $count; |
|
211 } |
|
212 |
|
213 /** |
|
214 * Checks the posts to see if this tweet has been attached to |
|
215 * any of them. |
|
216 * |
|
217 * @return bool |
|
218 */ |
|
219 function tweet_post_exists() { |
|
220 $posts = $this->get_post('post'); |
|
221 return (bool) (count($posts) > 0); |
|
222 } |
|
223 |
|
224 |
|
225 /** |
|
226 * Generate a GUID for WP post based on tweet ID. |
|
227 * |
|
228 * @return mixed |
|
229 */ |
|
230 static function guid_from_twid($tweet_id = null) { |
|
231 return (empty($tweet_id) ? false : 'http://twitter-'.$tweet_id); |
|
232 } |
|
233 |
|
234 |
|
235 /** |
|
236 * Generate a GUID for WP post based on this objects' tweet ID. |
|
237 * |
|
238 * @uses guid_from_twid |
|
239 * |
|
240 * @return mixed |
|
241 */ |
|
242 function guid() { |
|
243 return $this->guid_from_twid($this->id()); |
|
244 } |
|
245 |
|
246 |
|
247 /** |
|
248 * Grabs the post from the DB |
|
249 * |
|
250 * @uses get_posts |
|
251 * |
|
252 * @return obj|false |
|
253 */ |
|
254 function get_post($post_type = null) { |
|
255 if (isset($this->post)) { |
|
256 $this->post_id = $this->post->ID; |
|
257 return $this->post; |
|
258 } |
|
259 if (is_null($post_type)) { |
|
260 $post_type = AKTT::$post_type; |
|
261 } |
|
262 // TODO (future) - search by GUID instead? |
|
263 $posts = get_posts(array( |
|
264 'post_type' => $post_type, |
|
265 'meta_key' => '_aktt_tweet_id', |
|
266 'meta_value' => $this->id, |
|
267 )); |
|
268 if (!is_array($posts)) { |
|
269 return false; |
|
270 } |
|
271 else { |
|
272 $this->post = array_shift($posts); |
|
273 $this->post_id = $this->post->ID; |
|
274 return $this->post; |
|
275 } |
|
276 } |
|
277 |
|
278 |
|
279 /** |
|
280 * Twitter data changed - users still expect anything starting with @ is a reply |
|
281 * |
|
282 * @return bool |
|
283 */ |
|
284 function is_reply() { |
|
285 return (bool) (AKTT::substr($this->content(), 0, 1) == '@' || !empty($this->data->in_reply_to_screen_name)); |
|
286 } |
|
287 |
|
288 |
|
289 /** |
|
290 * Is this a retweet? |
|
291 * |
|
292 * @return bool |
|
293 */ |
|
294 function is_retweet() { |
|
295 return (bool) (AKTT::substr($this->content(), 0, 2) == 'RT' || !empty($this->data->retweeted_status)); |
|
296 } |
|
297 |
|
298 |
|
299 /** |
|
300 * Look up whether this tweet came from a broadcast |
|
301 * |
|
302 * @return bool |
|
303 */ |
|
304 function was_broadcast() { |
|
305 $was_broadcast = false; |
|
306 if (isset($this->data) && !empty($this->data->source)) { |
|
307 $was_broadcast = (bool) (strpos($this->data->source, 'sopresto.mailchimp.com') !== false); |
|
308 } |
|
309 else { |
|
310 $was_broadcast = (bool) (strpos($this->content(), home_url()) !== false); |
|
311 } |
|
312 return $was_broadcast; |
|
313 } |
|
314 |
|
315 function link_entities($defer_to_anywhere = true) { |
|
316 $entities = array(); |
|
317 // mentions |
|
318 $anywhere = Social::option('twitter_anywhere_api_key'); |
|
319 if (!$defer_to_anywhere || empty($anywhere) || is_feed()) { |
|
320 foreach ($this->mentions() as $entity) { |
|
321 $entities['start_'.str_pad($entity->indices[0], 5, '0', STR_PAD_LEFT)] = array( |
|
322 'find' => $entity->screen_name, |
|
323 'replace' => AKTT::profile_link($entity->screen_name), |
|
324 'start' => $entity->indices[0], |
|
325 'end' => $entity->indices[1], |
|
326 ); |
|
327 } |
|
328 } |
|
329 // hashtags |
|
330 foreach ($this->hashtags() as $entity) { |
|
331 $entities['start_'.str_pad($entity->indices[0], 5, '0', STR_PAD_LEFT)] = array( |
|
332 'find' => $entity->text, |
|
333 'replace' => AKTT::hashtag_link($entity->text), |
|
334 'start' => $entity->indices[0], |
|
335 'end' => $entity->indices[1], |
|
336 ); |
|
337 } |
|
338 // URLs |
|
339 foreach ($this->urls() as $entity) { |
|
340 $entities['start_'.str_pad($entity->indices[0], 5, '0', STR_PAD_LEFT)] = array( |
|
341 'find' => $entity->url, |
|
342 'replace' => '<a href="'.esc_url($entity->expanded_url).'">'.esc_html($entity->display_url).'</a>', |
|
343 'start' => $entity->indices[0], |
|
344 'end' => $entity->indices[1], |
|
345 ); |
|
346 } |
|
347 ksort($entities); |
|
348 $str = $this->content(); |
|
349 $diff = 0; |
|
350 foreach ($entities as $entity) { |
|
351 $start = $entity['start'] + $diff; |
|
352 $end = $entity['end'] + $diff; |
|
353 // $log = array(); |
|
354 // $log[] = 'diff: '.$diff; |
|
355 // $log[] = 'entity start: '.$entity['start']; |
|
356 // $log[] = 'entity start chars: '.AKTT::substr($this->content(), $entity['start'], 3); |
|
357 // $log[] = 'diff start: '.$start; |
|
358 // $log[] = 'diff start chars: '.AKTT::substr($str, $start, 3); |
|
359 // $log[] = 'entity end: '.$entity['end']; |
|
360 // $log[] = 'diff end: '.$end; |
|
361 // $log[] = 'find len: '.AKTT::strlen($entity['find']); |
|
362 // $log[] = 'find: '.htmlspecialchars($entity['find']); |
|
363 // $log[] = 'replace len: '.AKTT::strlen($entity['replace']); |
|
364 // $log[] = 'replace: '.htmlspecialchars($entity['replace']); |
|
365 // echo '<p>'.implode('<br>', $log).'</p>'; |
|
366 $str = AKTT::substr_replace($str, $entity['replace'], $start, ($end - $start)); |
|
367 $diff += AKTT::strlen($entity['replace']) - ($end - $start); |
|
368 } |
|
369 return $str; |
|
370 } |
|
371 |
|
372 |
|
373 /** |
|
374 * Parse tweet data and set taxonomies accordingly |
|
375 * |
|
376 * @param array $args |
|
377 * @return void |
|
378 */ |
|
379 function set_taxonomies() { |
|
380 if (empty($this->post_id)) { |
|
381 return; |
|
382 } |
|
383 $tax_input = array( |
|
384 'aktt_accounts' => array($this->username()), |
|
385 'aktt_hashtags' => array(), |
|
386 'aktt_mentions' => array(), |
|
387 'aktt_types' => array(), |
|
388 ); |
|
389 foreach ($this->hashtags() as $hashtag) { |
|
390 $tax_input['aktt_hashtags'][] = $hashtag->text; |
|
391 } |
|
392 foreach ($this->mentions() as $mention) { |
|
393 $tax_input['aktt_mentions'][] = $mention->screen_name; |
|
394 } |
|
395 $special = 0; |
|
396 if ($this->is_reply()) { |
|
397 $special++; |
|
398 $tax_input['aktt_types'][] = 'reply'; |
|
399 } |
|
400 else { |
|
401 $tax_input['aktt_types'][] = 'not-a-reply'; |
|
402 } |
|
403 if ($this->is_retweet()) { |
|
404 $special++; |
|
405 $tax_input['aktt_types'][] = 'retweet'; |
|
406 } |
|
407 else { |
|
408 $tax_input['aktt_types'][] = 'not-a-retweet'; |
|
409 } |
|
410 if ($this->was_broadcast()) { |
|
411 $special++; |
|
412 $tax_input['aktt_types'][] = 'social-broadcast'; |
|
413 } |
|
414 else { |
|
415 $tax_input['aktt_types'][] = 'not-a-social-broadcast'; |
|
416 } |
|
417 if (!$special) { |
|
418 $tax_input['aktt_types'][] = 'status'; |
|
419 } |
|
420 $tax_input = apply_filters('aktt_tweet_tax_input', $tax_input); |
|
421 foreach ($tax_input as $tax => $terms) { |
|
422 if (count($terms)) { |
|
423 wp_set_post_terms($this->post_id, $terms, $tax); |
|
424 } |
|
425 } |
|
426 } |
|
427 |
|
428 |
|
429 /** |
|
430 * Does this tweet have a photo? |
|
431 * |
|
432 * @return bool |
|
433 */ |
|
434 function has_image() { |
|
435 return ( |
|
436 !empty($this->data->entities->media) && |
|
437 $this->data->entities->media[0]->type == 'photo' |
|
438 ); |
|
439 } |
|
440 |
|
441 |
|
442 /** |
|
443 * Download and save tweet image |
|
444 * |
|
445 * @return mixed int|null |
|
446 */ |
|
447 function sideload_image() { |
|
448 if ($this->has_image()) { |
|
449 $url = $this->data->entities->media[0]->media_url; |
|
450 $id = aktt_sideload_image($url, $this->post_id); |
|
451 if (!is_wp_error($id)) { |
|
452 return $id; |
|
453 } |
|
454 } |
|
455 return null; |
|
456 } |
|
457 |
|
458 |
|
459 /** |
|
460 * Creates an aktt_tweet post_type with its meta |
|
461 * |
|
462 * @param array $args |
|
463 * @return void |
|
464 */ |
|
465 function add() { |
|
466 $gmt_time = self::twdate_to_time($this->date()); |
|
467 // Build the post data |
|
468 $data = apply_filters('aktt_tweet_add', array( |
|
469 'post_title' => $this->title(), |
|
470 'post_name' => $this->id(), |
|
471 'post_content' => $this->content(), |
|
472 'post_status' => 'publish', |
|
473 'post_type' => AKTT::$post_type, |
|
474 'post_date' => date('Y-m-d H:i:s', AKTT::gmt_to_wp_time($gmt_time)), |
|
475 'post_date_gmt' => date('Y-m-d H:i:s', $gmt_time), |
|
476 'guid' => $this->guid(), |
|
477 // 'tax_input' => $tax_input, // see below... |
|
478 )); |
|
479 $post_id = wp_insert_post(addslashes_deep($data), true); |
|
480 if (is_wp_error($post_id)) { |
|
481 AKTT::log('WP_Error:: '.$post_id->get_error_message()); |
|
482 return false; |
|
483 } |
|
484 $this->post_id = $post_id; |
|
485 |
|
486 // have to set up taxonomies after the insert in case we are in a context without |
|
487 // a 'current user' - see: http://core.trac.wordpress.org/ticket/19373 |
|
488 $this->set_taxonomies(); |
|
489 |
|
490 // if there is a photo, add it |
|
491 $this->featured_image_id = $this->sideload_image(); |
|
492 if (!empty($this->featured_image_id)) { |
|
493 update_post_meta($this->post_id, '_thumbnail_id', $this->featured_image_id); |
|
494 } |
|
495 |
|
496 update_post_meta($this->post_id, '_aktt_tweet_id', $this->id()); |
|
497 update_post_meta($this->post_id, '_aktt_tweet_raw_data', addslashes($this->raw_data)); |
|
498 |
|
499 // Allow things to hook in here |
|
500 do_action('AKTT_Tweet_added', $this); |
|
501 |
|
502 return true; |
|
503 } |
|
504 |
|
505 |
|
506 /** |
|
507 * Replace the raw Twitter data for a tweet |
|
508 * |
|
509 * @param stdClass $tweet_data |
|
510 * @return bool |
|
511 */ |
|
512 function update_twitter_data($tweet_data) { |
|
513 $this->data = $tweet_data; |
|
514 $this->raw_data = json_encode($tweet_data); |
|
515 $post = $this->get_post(); |
|
516 if ($post && !empty($post->ID)) { |
|
517 if (update_post_meta($post->ID, '_aktt_tweet_raw_data', addslashes($this->raw_data))) { |
|
518 delete_post_meta($post->ID, '_aktt_30_backfill_needed', 1); |
|
519 $this->set_taxonomies(); |
|
520 return true; |
|
521 } |
|
522 } |
|
523 return false; |
|
524 } |
|
525 |
|
526 |
|
527 /** |
|
528 * Create blog post from tweet |
|
529 * |
|
530 * @param array $args |
|
531 * @return bool |
|
532 */ |
|
533 function create_blog_post($args = array()) { |
|
534 extract($args); |
|
535 |
|
536 // Add a space if we have a prefix |
|
537 $title_prefix = empty($title_prefix) ? '' : trim($title_prefix).' '; |
|
538 |
|
539 $post_content = $this->link_entities(false); |
|
540 // Append image to post if there is one, can't set it as a featured image until after save |
|
541 if (!empty($this->featured_image_id)) { |
|
542 $size = apply_filters('aktt_featured_image_size', 'medium'); |
|
543 $post_content .= "\n\n".wp_get_attachment_image($this->featured_image_id, $size); |
|
544 } |
|
545 |
|
546 $gmt_time = self::twdate_to_time($this->date()); |
|
547 |
|
548 // Build the post data |
|
549 $data = array( |
|
550 'post_title' => $title_prefix.$this->title(), |
|
551 'post_content' => $post_content, |
|
552 'post_author' => $post_author, |
|
553 // see below |
|
554 // 'tax_input' => array( |
|
555 // 'category' => array($post_category), |
|
556 // 'post_tag' => array_map('trim', explode(',', $post_tags)), |
|
557 // ), |
|
558 'post_status' => 'publish', |
|
559 'post_type' => 'post', |
|
560 'post_date' => date('Y-m-d H:i:s', AKTT::gmt_to_wp_time($gmt_time)), |
|
561 'post_date_gmt' => date('Y-m-d H:i:s', $gmt_time), |
|
562 'guid' => $this->guid().'-post' |
|
563 ); |
|
564 $data = apply_filters('aktt_tweet_create_blog_post_data', $data); |
|
565 |
|
566 // hook in here if you want to conditionally skip blog post creation |
|
567 if (!apply_filters('aktt_tweet_create_blog_post', true, $data, $this)) { |
|
568 return false; |
|
569 } |
|
570 |
|
571 $this->blog_post_id = wp_insert_post($data, true); |
|
572 |
|
573 if (is_wp_error($this->blog_post_id)) { |
|
574 AKTT::log('WP_Error:: '.$this->blog_post_id->get_error_message()); |
|
575 return false; |
|
576 } |
|
577 |
|
578 // have to set up taxonomies after the insert in case we are in a context without |
|
579 // a 'current user' - see: http://core.trac.wordpress.org/ticket/19373 |
|
580 wp_set_object_terms($this->blog_post_id, intval($post_category), 'category'); |
|
581 wp_set_object_terms($this->blog_post_id, array_map('trim', explode(',', $post_tags)), 'post_tag'); |
|
582 |
|
583 // hook in here and return false to not set the format to "status", |
|
584 // or return another format to use that format instead of status |
|
585 if ($post_format = apply_filters('aktt_tweet_create_blog_post_format', 'status', $data, $this)) { |
|
586 set_post_format($this->blog_post_id, $post_format); |
|
587 } |
|
588 |
|
589 if (!empty($this->featured_image_id)) { |
|
590 update_post_meta($this->blog_post_id, '_thumbnail_id', $this->featured_image_id); |
|
591 } |
|
592 |
|
593 update_post_meta($this->blog_post_id, '_aktt_tweet_id', $this->id()); // twitter's tweet ID |
|
594 update_post_meta($this->blog_post_id, '_aktt_tweet_post_id', $this->post_id); // twitter's post ID |
|
595 |
|
596 // Add it to the tweet's post_meta as well |
|
597 update_post_meta($this->post_id, '_aktt_tweet_blog_post_id', $this->blog_post_id); |
|
598 |
|
599 // Let Social know to aggregate info about this post |
|
600 $account = false; |
|
601 foreach (AKTT::$accounts as $aktt_account) { |
|
602 if ($aktt_account->social_acct->id() == $this->data->user->id_str) { |
|
603 $account = $aktt_account->social_acct; |
|
604 break; |
|
605 } |
|
606 } |
|
607 if ($account) { |
|
608 Social::instance()->add_broadcasted_id( |
|
609 $this->blog_post_id, |
|
610 'twitter', |
|
611 $this->id(), |
|
612 $this->content(), |
|
613 $account, |
|
614 null |
|
615 ); |
|
616 } |
|
617 |
|
618 // Let the account know we were successful |
|
619 return true; |
|
620 } |
|
621 |
|
622 |
|
623 /** |
|
624 * Gets the name of a tag from its ID |
|
625 * |
|
626 * @param int $tag_id |
|
627 * @return string |
|
628 */ |
|
629 function get_tag_name($tag_id) { |
|
630 $tag_name = get_term_field('name', $tag_id, 'post_tag', 'db'); |
|
631 if (is_wp_error($tag_name)) { |
|
632 $tag_name = ''; |
|
633 } |
|
634 return $tag_name; |
|
635 } |
|
636 |
|
637 } |
|
638 |