1: <?php
2: 3: 4: 5: 6: 7: 8:
9: $plugin_is_filter = 9 | FEATURE_PLUGIN;
10: $plugin_description = gettext('Tweet news articles when published.');
11: $plugin_author = "Stephen Billard (sbillard)";
12: $plugin_disable = (function_exists('curl_init')) ? false : gettext('The <em>php_curl</em> extension is required');
13: $plugin_notice = (extensionEnabled('TinyURL')) ? '' : gettext('Enable the tinyURL plugin to shorten URLs in your tweets.');
14:
15: $option_interface = 'tweet';
16:
17: if ($plugin_disable) {
18: enableExtension('tweet_news', 0);
19: } else {
20: zp_register_filter('show_change', 'tweet::published');
21: if (getOption('tweet_news_albums'))
22: zp_register_filter('new_album', 'tweet::published');
23: if (getOption('tweet_news_images'))
24: zp_register_filter('new_image', 'tweet::published');
25: if (getOption('tweet_news_news'))
26: zp_register_filter('new_article', 'tweet::newZenpageObject');
27: if (getOption('tweet_news_pages'))
28: zp_register_filter('new_page', 'tweet::newZenpageObject');
29: zp_register_filter('admin_head', 'tweet::scan');
30: zp_register_filter('load_theme_script', 'tweet::scan');
31: zp_register_filter('admin_overview', 'tweet::errorsOnOverview');
32: zp_register_filter('admin_note', 'tweet::errorsOnAdmin');
33: zp_register_filter('edit_album_utilities', 'tweet::tweeter');
34: zp_register_filter('save_album_utilities_data', 'tweet::tweeterExecute');
35: zp_register_filter('edit_image_utilities', 'tweet::tweeter');
36: zp_register_filter('save_image_utilities_data', 'tweet::tweeterExecute');
37: zp_register_filter('general_zenpage_utilities', 'tweet::tweeter');
38: zp_register_filter('save_article_custom_data', 'tweet::tweeterZenpageExecute');
39: zp_register_filter('save_page_custom_data', 'tweet::tweeterZenpageExecute');
40:
41: require_once(getPlugin('tweet_news/twitteroauth.php'));
42: }
43:
44: 45: 46: 47: 48: 49:
50: class tweet {
51:
52: function __construct() {
53: setOptionDefault('tweet_news_consumer', NULL);
54: setOptionDefault('tweet_news_consumer_secret', NULL);
55: setOptionDefault('tweet_news_oauth_token', NULL);
56: setOptionDefault('tweet_news_oauth_token_secret', NULL);
57: setOptionDefault('tweet_news_rescan', 1);
58: setOptionDefault('tweet_news_categories_none', NULL);
59: setOptionDefault('tweet_news_images', NULL);
60: setOptionDefault('tweet_news_albums', NULL);
61: setOptionDefault('tweet_news_news', 1);
62: setOptionDefault('tweet_news_protected', NULL);
63: setOptionDefault('tweet_news_pages', 0);
64: setOptionDefault('tweet_language', NULL);
65: }
66:
67: 68: 69: 70:
71: function getOptionsSupported() {
72: global $_zp_zenpage;
73: $options = array(gettext('Consumer key') => array('key' => 'tweet_news_consumer', 'type' => OPTION_TYPE_TEXTBOX,
74: 'order' => 2,
75: 'desc' => gettext('This <code>tweet_news</code> app for this site needs a <em>consumer key</em>, a <em>consumer key secret</em>, an <em>access token</em>, and an <em>access token secret</em>.') . '<p class="notebox">' . gettext('Get these from <a href="http://dev.twitter.com/">Twitter developers</a>') . '</p>'),
76: gettext('Secret') => array('key' => 'tweet_news_consumer_secret', 'type' => OPTION_TYPE_TEXTBOX,
77: 'order' => 3,
78: 'desc' => gettext('The <em>secret</em> associated with your <em>consumer key</em>.')),
79: gettext('Access token') => array('key' => 'tweet_news_oauth_token', 'type' => OPTION_TYPE_TEXTBOX,
80: 'order' => 4,
81: 'desc' => gettext('The application <em>oauth_token</em> token.')),
82: gettext('Access token secret') => array('key' => 'tweet_news_oauth_token_secret', 'type' => OPTION_TYPE_TEXTBOX,
83: 'order' => 5,
84: 'desc' => gettext('The application <em>oauth_token</em> secret.')),
85: gettext('Protected objects') => array('key' => 'tweet_news_protected', 'type' => OPTION_TYPE_CHECKBOX,
86: 'order' => 7,
87: 'desc' => gettext('If checked, protected items will be tweeted. <strong>Note:</strong> followers will need the password to visit the tweeted link.'))
88: );
89: $note = '';
90: $list = array('<em>' . gettext('Albums') . '</em>' => 'tweet_news_albums', '<em>' . gettext('Images') . '</em>' => 'tweet_news_images');
91: if (extensionEnabled('zenpage')) {
92: $list['<em>' . gettext('News') . '</em>'] = 'tweet_news_news';
93: $list['<em>' . gettext('Pages') . '</em>'] = 'tweet_news_pages';
94: $options[gettext('Scan pending')] = array('key' => 'tweet_news_rescan', 'type' => OPTION_TYPE_CHECKBOX,
95: 'order' => 8,
96: 'desc' => gettext('<code>tweet_news</code> notices when a page or an article is published. ' .
97: 'If the date is in the future, it is put in the <em>to-be-tweeted</em> and tweeted when that date arrives. ' .
98: 'This option allows you to re-populate that list to the current state of scheduled tweets.')
99: );
100: } else {
101: setOption('tweet_news_news', 0);
102: setOption('tweet_news_pages', 0);
103: }
104: $options[gettext('Tweet')] = array('key' => 'tweet_news_items', 'type' => OPTION_TYPE_CHECKBOX_ARRAY,
105: 'order' => 6,
106: 'checkboxes' => $list,
107: 'desc' => gettext('If a <em>type</em> is checked, a Tweet will be made when an object of that <em>type</em> is published.'));
108: If (getOption('multi_lingual')) {
109: $options[gettext('Tweet Language')] = array('key' => 'tweet_language', 'type' => OPTION_TYPE_SELECTOR,
110: 'order' => 5.5,
111: 'selections' => generateLanguageList(),
112: 'desc' => gettext('Select the language for the Tweet message.'));
113: }
114: if (getOption('tweet_news_news') && is_object($_zp_zenpage)) {
115: $catlist = getSerializedArray(getOption('tweet_news_categories'));
116: $news_categories = $_zp_zenpage->getAllCategories(false);
117: $catlist = array(gettext('*not categorized*') => 'tweet_news_categories_none');
118: foreach ($news_categories as $category) {
119: $option = 'tweet_news_categories_' . $category['titlelink'];
120: $catlist[$category['title']] = $option;
121: setOptionDefault($option, NULL);
122: }
123: $options[gettext('News categories')] = array('key' => 'tweet_news_categories', 'type' => OPTION_TYPE_CHECKBOX_UL,
124: 'order' => 6.5,
125: 'checkboxes' => $catlist,
126: 'desc' => gettext('Only those <em>news categories</em> checked will be Tweeted. <strong>Note:</strong> <em>*not categorized*</em> means those news articles which have no category assigned.'));
127: }
128: if (getOption('tweet_news_rescan')) {
129: setOption('tweet_news_rescan', 0);
130: $note = self::tweetRepopulate();
131: }
132: if ($note) {
133: $options['note'] = array('key' => 'tweet_news_rescan', 'type' => OPTION_TYPE_NOTE,
134: 'order' => 0,
135: 'desc' => $note);
136: }
137:
138: return $options;
139: }
140:
141: 142: 143: 144: 145: 146:
147: function handleOption($option, $currentValue) {
148:
149: }
150:
151: 152: 153: 154: 155:
156: private static function sendTweet($status) {
157: global $tweet;
158: if (!is_object($tweet)) {
159: $consumerKey = getOption('tweet_news_consumer');
160: $consumerSecret = getOption('tweet_news_consumer_secret');
161: $OAuthToken = getOption('tweet_news_oauth_token');
162: $OAuthSecret = getOption('tweet_news_oauth_token_secret');
163: $tweet = new TwitterOAuth($consumerKey, $consumerSecret, $OAuthToken, $OAuthSecret);
164: }
165: $response = $tweet->post('statuses/update', array('status' => $status));
166: if (isset($response->error)) {
167: return $response->error;
168: }
169: return false;
170: }
171:
172: 173: 174: 175: 176: 177:
178: static function newZenpageObject($msg, $article) {
179: $error = self::tweetObjectWithCheck($article);
180: if ($error) {
181: $msg .= '<p class="errorbox">' . $error . '</p>';
182: }
183: return $msg;
184: }
185:
186: 187: 188: 189: 190:
191: static function published($obj) {
192: $error = self::tweetObjectWithCheck($obj);
193: if ($error) {
194: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","error",' . db_quote($error) . ')');
195: }
196: return $obj;
197: }
198:
199: 200: 201: 202: 203:
204: private static function tweetObjectWithCheck($obj) {
205: $error = '';
206: $type = $obj->table;
207: if (getOption('tweet_news_' . $type)) {
208: if ($obj->getShow()) {
209: if (getOption('tweet_news_protected') || !$obj->isProtected()) {
210: switch ($type = $obj->table) {
211: case 'pages':
212: $dt = $obj->getDateTime();
213: if ($dt > date('Y-m-d H:i:s')) {
214: $result = query_single_row('SELECT * FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_pages" AND `data`=' . db_quote($obj->getTitlelink()));
215: if (!$result) {
216: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_pages",' . db_quote($obj->getTitlelink()) . ')');
217: }
218: } else {
219: $error = self::tweetObject($obj);
220: }
221: break;
222: case 'news':
223: $tweet = false;
224: $mycategories = $obj->getCategories();
225: if (empty($mycategories)) {
226: $tweet = getOption('tweet_news_categories_none');
227: } else {
228: foreach ($mycategories as $cat) {
229: if ($tweet = getOption('tweet_news_categories_' . $cat['titlelink'])) {
230: break;
231: }
232: }
233: }
234: if (!$tweet) {
235: break;
236: }
237: $dt = $obj->getDateTime();
238: if ($dt > date('Y-m-d H:i:s')) {
239: $result = query_single_row('SELECT * FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending" AND `data`=' . db_quote($obj->getTitlelink()));
240: if (!$result) {
241: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending",' . db_quote($obj->getTitlelink()) . ')');
242: }
243: } else {
244: $error = self::tweetObject($obj);
245: }
246: break;
247: case 'albums':
248: $dt = $obj->getPublishDate();
249: if ($dt > date('Y-m-d H:i:s')) {
250: $result = query_single_row('SELECT * FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_albums" AND `data`=' . db_quote($obj->name));
251: if (!$result) {
252: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_albums",' . db_quote($obj->name) . ')');
253: }
254: } else {
255: $error = self::tweetObject($obj);
256: }
257: break;
258: case 'images':
259: $dt = $obj->getPublishDate();
260: if ($dt > date('Y-m-d H:i:s')) {
261: $result = query_single_row('SELECT * FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_images" AND `data`=' . db_quote($obj->imagefolder . '/' . $obj->filename));
262: if (!$result) {
263: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_images",' . db_quote($obj->imagefolder . '/' . $obj->filename) . ')');
264: }
265: } else {
266: $error = self::tweetObject($obj);
267: }
268: break;
269: }
270: }
271: }
272: }
273: return $error;
274: }
275:
276: 277: 278: 279: 280: 281: 282: 283:
284: private static function composeStatus($link, $title, $text) {
285: $text = trim(html_decode(getBare($text)));
286: if ($title) {
287: $title = trim(html_decode(getBare($title))) . ': ';
288: }
289: if (strlen($title . $text . ' ' . $link) > 140) {
290: $c = 140 - strlen($link);
291: if (mb_strlen($title) >= ($c - 25)) {
292: $text = truncate_string($title, $c - 4, '... ') . $link;
293: } else {
294: $c = $c - mb_strlen($title) - 5;
295: $text = $title . truncate_string($text, $c, '... ') . $link;
296: }
297: } else {
298: $text = $title . $text . ' ' . $link;
299: }
300: $error = self::sendTweet($text);
301: if ($error) {
302: $error = sprintf(gettext('Error tweeting <code>%1$s</code>: %2$s'), $text, $error);
303: }
304: return $error;
305: }
306:
307: 308: 309: 310: 311:
312: private static function tweetObject($obj) {
313: if (getOption('multi_lingual')) {
314: $cur_locale = getUserLocale();
315: setupCurrentLocale(getOption('tweet_language'));
316: }
317: $error = '';
318: if (class_exists('tinyURL')) {
319: $link = tinyURL::getURL($obj);
320: } else {
321: $link = $obj->getLink();
322: }
323: switch ($type = $obj->table) {
324: case 'pages':
325: case 'news':
326: $error = self::composeStatus($link, $obj->getTitle(), $obj->getContent());
327: break;
328: case 'albums':
329: case 'images':
330: if ($type == 'images') {
331: $text = sprintf(gettext('New image: [%2$s]%1$s '), $item = $obj->getTitle(), $obj->imagefolder);
332: } else {
333: $text = sprintf(gettext('New album: %s '), $item = $obj->getTitle());
334: }
335: $error = self::composeStatus($link, '', $item);
336: break;
337: case 'comments':
338: $error = self::composeStatus($link, '', $obj->getComment());
339: break;
340: }
341: if (isset($cur_locale)) {
342: setupCurrentLocale($cur_locale);
343: }
344: return $error;
345: }
346:
347: 348: 349: 350: 351: 352: 353:
354: static function scan($script, $valid = true) {
355: if ($script && $valid) {
356: $result = query_full_array('SELECT * FROM ' . prefix('news') . ' AS news,' . prefix('plugin_storage') . ' AS store WHERE store.type="tweet_news" AND store.aux="pending" AND store.data = news.titlelink AND news.date <= ' . db_quote(date('Y-m-d H:i:s')));
357: if ($result) {
358: foreach ($result as $article) {
359: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `id`=' . $article['id']);
360: $news = new ZenpageNews($article['titlelink']);
361: self::tweetObject($news);
362: }
363: }
364: $result = query_full_array('SELECT * FROM ' . prefix('pages') . ' AS page,' . prefix('plugin_storage') . ' AS store WHERE store.type="tweet_news" AND store.aux="pending_pages" AND store.data = page.titlelink AND page.date <= ' . db_quote(date('Y-m-d H:i:s')));
365: if ($result) {
366: foreach ($result as $page) {
367: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `id`=' . $page['id']);
368: $page = new ZenpageNews($page['titlelink']);
369: self::tweetObject($page);
370: }
371: }
372: $result = query_full_array('SELECT * FROM ' . prefix('albums') . ' AS album,' . prefix('plugin_storage') . ' AS store WHERE store.type="tweet_news" AND store.aux="pending_albums" AND store.data = album.folder AND album.date <= ' . db_quote(date('Y-m-d H:i:s')));
373: if ($result) {
374: foreach ($result as $album) {
375: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `id`=' . $album['id']);
376: $album = newAlbum($album['folder']);
377: self::tweetObject($album);
378: }
379: }
380: $result = query_full_array('SELECT * FROM ' . prefix('images') . ' AS image,' . prefix('plugin_storage') . ' AS store WHERE store.type="tweet_news" AND store.aux="pending_images" AND store.data LIKE image.filename AND image.date <= ' . db_quote(date('Y-m-d H:i:s')));
381: if ($result) {
382: foreach ($result as $image) {
383: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `id`=' . $image['id']);
384: $album = query_single_row('SELECT * FROM ' . prefix('albums') . ' WHERE `id`=' . $image['albumid']);
385: $album = newAlbum($album['folder']);
386: $image = newImage($album, $image['filename']);
387: self::tweetObject($image);
388: }
389: }
390: }
391: return $script;
392: }
393:
394: 395: 396: 397:
398: private static function tweetRepopulate() {
399: $found = array();
400: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending"');
401: $result = query_full_array('SELECT * FROM ' . prefix('news') . ' WHERE `show`=1 AND `date`>' . db_quote(date('Y-m-d H:i:s')));
402: if ($result) {
403: foreach ($result as $pending) {
404: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending",' . db_quote($pending['titlelink']) . ')');
405: }
406: $found[] = gettext('news');
407: }
408: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_pages"');
409: $result = query_full_array('SELECT * FROM ' . prefix('pages') . ' WHERE `show`=1 AND `date`>' . db_quote(date('Y-m-d H:i:s')));
410: if ($result) {
411: foreach ($result as $pending) {
412: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_pages",' . db_quote($pending['titlelink']) . ')');
413: }
414: $found[] = gettext('pages');
415: }
416: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_albums"');
417: $result = query_full_array('SELECT * FROM ' . prefix('albums') . ' WHERE `show`=1 AND `date`>' . db_quote(date('Y-m-d H:i:s')));
418: if ($result) {
419: foreach ($result as $pending) {
420: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_albums",' . db_quote($pending['folder']) . ')');
421: }
422: $found[] = gettext('albums');
423: }
424: query('DELETE FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="pending_images"');
425: $result = query_full_array('SELECT * FROM ' . prefix('images') . ' WHERE `show`=1 AND `date`>' . db_quote(date('Y-m-d H:i:s')));
426: if ($result) {
427: foreach ($result as $pending) {
428: $album = query_single_row('SELECT * FROM ' . prefix('albums') . ' WHERE `id`=' . $pending['albumid']);
429: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","pending_images",' . db_quote($album['folder'] . '/' . $pending['filename']) . ')');
430: }
431: $found[] = gettext('images');
432: }
433: if (empty($found)) {
434: return '<p class="messagebox">' . gettext('No scheduled news articles found.') . '</p>';
435: } else {
436: return '<p class="messagebox">' . gettext('Scheduled items have been noted for tweeting.') . '</p>';
437: }
438: }
439:
440: 441: 442: 443:
444: private static function tweetFetchErrors() {
445: $result = query_full_array('SELECT * FROM ' . prefix('plugin_storage') . ' WHERE `type`="tweet_news" AND `aux`="error"');
446: $errors = '';
447: foreach ($result as $error) {
448: $errors .= $error['data'] . '<br />';
449: query('DELETE FROM' . prefix('plugin_storage') . ' WHERE `id`=' . $error['id']);
450: }
451: return $errors;
452: }
453:
454: 455: 456: 457: 458:
459: static function errorsOnOverview($side) {
460: if ($side == 'left') {
461: $errors = self::tweetFetchErrors();
462: if ($errors) {
463: ?>
464: <div class="box" id="overview-news">
465: <h2 class="h2_bordered"><?php echo gettext("Tweet News Errors:"); ?></h2>
466: <?php echo '<p class="errorbox">' . $errors . '</p>'; ?>
467: </div>
468: <?php
469: }
470: }
471: return $side;
472: }
473:
474: 475: 476: 477: 478: 479:
480: static function errorsOnAdmin($tab, $subtab) {
481: $errors = self::tweetFetchErrors();
482: if ($errors) {
483: echo '<p class="errorbox">' . $errors . '</p>';
484: }
485: return $tab;
486: }
487:
488: 489: 490: 491: 492: 493: 494:
495: static function tweeter($before, $object, $prefix = NULL) {
496: $output = '<p class="checkbox">' . "\n" . '<label>' . "\n" . '<input type="checkbox" name="tweet_me' . $prefix . '" id="tweet_me' . $prefix . '" value="1" /> <img src="' . WEBPATH . '/' . ZENFOLDER . '/' . PLUGIN_FOLDER . '/tweet_news/twitter_newbird_blue.png" /> ' . gettext('Tweet me') . "\n</label>\n</p>\n";
497: return $before . $output;
498: }
499:
500: 501: 502: 503: 504: 505:
506: static function tweeterExecute($object, $prefix) {
507: if (isset($_POST['tweet_me' . $prefix])) {
508: $error = self::tweetObject($object);
509: if ($error) {
510: query('INSERT INTO ' . prefix('plugin_storage') . ' (`type`,`aux`,`data`) VALUES ("tweet_news","error",' . db_quote($error) . ')');
511: }
512: }
513: return $object;
514: }
515:
516: 517: 518: 519: 520: 521:
522: static function tweeterZenpageExecute($custom, $object) {
523: self::tweeterExecute($object, '');
524: return $custom;
525: }
526:
527: }
528: ?>