@serendipity_plugin_api::load_language(dirname(__FILE__)); include_once dirname(__FILE__) . '/plugin_version.inc.php'; require_once dirname(__FILE__) . '/classes/Twitter.php'; include_once dirname(__FILE__) . '/classes/json.php4.include.php'; class serendipity_plugin_twitter extends serendipity_plugin { var $title = PLUGIN_TWITTER_TITLE; var $debug = false; function introspect(&$propbag) { $this->title = $this->get_config('title', $this->title); $propbag->add('name', PLUGIN_TWITTER_TITLE); $propbag->add('description', PLUGIN_TWITTER_DESC); $propbag->add('stackable', false); $propbag->add('author', 'Grischa Brockhaus, Damian Luszczymak, Garvin Hicking'); $propbag->add('version', PLUGIN_TWITTER_VERSION); $propbag->add('requirements', array( 'serendipity' => '0.8', 'smarty' => '2.6.7', 'php' => '5.1.0' )); $configuration = array('title', 'number', 'service', 'username', 'showformat', 'toall_only', 'filter_all_user', 'filter_rt', 'use_time_ago', 'dateformat', 'linktext', 'followme_link', 'followme_widget', 'followme_widget_counter','followme_widget_dark', 'cachetime', 'backup' ); // Twitter API 1.1 is supported only via the event plugin if (class_exists('serendipity_event_twitter')) { $configuration = array_merge($configuration, array('twitter_api' , 'twitter_oauth')); } if (!class_exists('serendipity_event_twitter')) { $configuration = array_merge($configuration, array('event_not_installed')); } $propbag->add('configuration', $configuration); $propbag->add('groups', array('FRONTEND_VIEWS')); } function introspect_config_item($name, &$propbag) { global $serendipity; switch($name) { case 'event_not_installed': $propbag->add('type', 'content'); $propbag->add('default', PLUGIN_TWITTER_EVENT_NOT_INSTALLED); break; break; case 'title': $propbag->add('type', 'string'); $propbag->add('name', TITLE); $propbag->add('description', TITLE_FOR_NUGGET); $propbag->add('default', PLUGIN_TWITTER_TITLE); break; case 'service': $propbag->add('type', 'radio'); $propbag->add('name', PLUGIN_TWITTER_SERVICE); $propbag->add('description', PLUGIN_TWITTER_SERVICE_DESC); $propbag->add('radio', array( 'value' => array('twitter.com', 'identi.ca'), 'desc' => array('Twitter.com', 'identi.ca') )); $propbag->add('default', 'twitter.com'); break; case 'twitter_api': $propbag->add('type', 'radio'); $propbag->add('name', PLUGIN_TWITTER_API11); $propbag->add('description', PLUGIN_TWITTER_API11_DESC); $propbag->add('radio', array( 'value' => array('1.0', '1.1'), 'desc' => array('API 1.0 [depr]', 'API 1.1') )); $propbag->add('default', '1.0'); break; case 'twitter_oauth': $propbag->add('name', PLUGIN_TWITTER_OAUTHACC); $propbag->add('description', PLUGIN_TWITTER_OAUTHACC_DESC); $propbag->add('type', 'select'); $propbag->add('select_values', serendipity_event_twitter::getTwitterOauths()); $propbag->add('default', '1'); break; case 'username': $propbag->add('type', 'string'); $propbag->add('name', PLUGIN_TWITTER_USERNAME); $propbag->add('description', PLUGIN_TWITTER_USERNAME_DESC); $propbag->add('default', 'username'); break; case 'number': $propbag->add('type', 'string'); $propbag->add('name', PLUGIN_TWITTER_NUMBER); $propbag->add('description', PLUGIN_TWITTER_NUMBER_DESC); $propbag->add('default', 10); break; case 'toall_only': // filter only tweets starting with @ $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_TOALL_ONLY); $propbag->add('description', PLUGIN_TWITTER_TOALL_ONLY_DESC); $propbag->add('default', false); break; case 'filter_all_user': // filter tweets containing @ $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FILTER_ALL); $propbag->add('description', PLUGIN_TWITTER_FILTER_ALL_DESC); $propbag->add('default', false); break; case 'filter_rt': // filter native retweets $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FILTER_RT); $propbag->add('description', PLUGIN_TWITTER_FILTER_RT_DESC); $propbag->add('default', true); break; case 'followme_link': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FOLLOWME_LINK); $propbag->add('description', PLUGIN_TWITTER_FOLLOWME_LINK_DESC); $propbag->add('default', false); break; case 'followme_widget': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FOLLOWME_WIDGET); $propbag->add('description', PLUGIN_TWITTER_FOLLOWME_WIDGET_DESC); $propbag->add('default', false); break; case 'followme_widget_counter': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FOLLOWME_WIDGET_COUNT); $propbag->add('description', PLUGIN_TWITTER_FOLLOWME_WIDGET_COUNT_DESC); $propbag->add('default', true); break; case 'followme_widget_dark': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_FOLLOWME_WIDGET_DARK); $propbag->add('description', PLUGIN_TWITTER_FOLLOWME_WIDGET_DARK_DESC); $propbag->add('default', false); break; case 'use_time_ago': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_USE_TIME_AGO); $propbag->add('description', PLUGIN_TWITTER_USE_TIME_AGO_DESC); $propbag->add('default', 'false'); break; case 'dateformat': $propbag->add('type', 'string'); $propbag->add('name', GENERAL_PLUGIN_DATEFORMAT); $propbag->add('description', sprintf(GENERAL_PLUGIN_DATEFORMAT_BLAHBLAH, '%A, %B %e %Y')); $propbag->add('default', '%A, %B %e %Y'); break; case 'linktext': $propbag->add('type', 'string'); $propbag->add('name', PLUGIN_TWITTER_LINKTEXT); $propbag->add('description', PLUGIN_TWITTER_LINKTEXT_DESC); $propbag->add('default', 'link'); break; case 'cachetime': $propbag->add('type', 'string'); $propbag->add('name', PLUGIN_TWITTER_CACHETIME); $propbag->add('description', PLUGIN_TWITTER_CACHETIME_DESC); $propbag->add('default', '300'); break; case 'showformat': $propbag->add('type', 'radio'); $propbag->add('name', PLUGIN_TWITTER_SHOWFORMAT); $propbag->add('description', PLUGIN_TWITTER_SHOWFORMAT_DESC); $propbag->add('radio', array( 'value' => array('javascript', 'PHP'), 'desc' => array(PLUGIN_TWITTER_SHOWFORMAT_RADIO_JAVASCRIPT, PLUGIN_TWITTER_SHOWFORMAT_RADIO_PHP) )); $propbag->add('default', 'javascript'); break; case 'backup': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_TWITTER_BACKUP); $propbag->add('description', PLUGIN_TWITTER_BACKUP_DESC); $propbag->add('default', 'false'); break; default: return false; } return true; } function cleanup() { global $serendipity; $service = $this->get_config('service', 'twitter.com'); $username = $this->get_config('username'); // If followme widget is set, disable linḱ if ($service=='twitter.com' && serendipity_db_bool($this->get_config('followme_widget'))) { $this->set_config('followme_link', FALSE); } $cache_user = md5($service) . md5($username); $cachefile = $serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . "/twitterresult.$cache_user.json"; // Remove Cachefile @unlink($cachefile); // Recreate cache serendipity_request_start(); $title = "#caching#"; $this->generate_content($title); serendipity_request_end(); } function output($out) { if (LANG_CHARSET == 'UTF-8') { echo $out; } else { echo utf8_decode($out); } } function generate_content(&$title) { global $serendipity; $hideDisplay = "#caching#" == $title; $number = $this->get_config('number'); $service = $this->get_config('service', 'twitter.com'); $username = $this->get_config('username'); $dateformat = $this->get_config('dateformat'); $title = $this->get_config('title', $this->title); $showformat = $this->get_config('showformat'); $cachetime = (int)$this->get_config('cachetime', 300); if (!is_numeric($number)) { $number = 10; } if ($service == 'identi.ca') { $followme_url = 'http://identi.ca/' . $username; $service_url = 'http://identi.ca/api'; $status_url = 'http://identi.ca/notice/'; //$JSONcallback = 'identicaCallback2'; $JSONcallback = 'twitterCallback2'; // We call the twitter widget. It is working with identi.ca too, but the callback name is twitter! $timelineurl = 'http://identi.ca/api/statuses/user_timeline/' . $username . '.json?callback=' . $JSONcallback . '&count=' . $number; $api = new Twitter(true); } else // if ($this->get_config('twitter_api','1.0') == '1.0') { $followme_url = 'https://twitter.com/' . $username; $service_url = 'https://twitter.com'; $status_url = 'https://twitter.com/' . $username . '/statuses/'; $JSONcallback = 'twitterCallback2'; $timelineurl = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $username . '&count=' . $number . '&callback=' . $JSONcallback; $api = new Twitter(false); } if (!$dateformat || strlen($dateformat) < 1) { $dateformat = '%A, %B %e %Y'; } if ($showformat == 'PHP') { $cache_user = md5($service) . md5($username); $cachefile = $serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . "/twitterresult.$cache_user.json"; // If the Event Plugin is not installed, we have to fill the cachefile on our own.. // To immidiately display a result, the file_exists check is added. if (!class_exists('serendipity_event_twitter') || !file_exists($cachefile)) { // If we have twitter using API 1.1 the sidebarplugin *needs* the eventplugin to update if ($this->get_config('service','twitter.com') == 'identi.ca' || $this->get_config('twitter_api','1.0') == '1.0') { $this->updateTwitterTimelineCache($cachefile); } } // Get xml from cache: if (file_exists($cachefile)) { $xml = json_decode(unserialize(file_get_contents($cachefile))); } $str_output = array(); // now process it: $str_output[] = ''; // Display only, if we have something meaningful: if (!$hideDisplay && count($str_output)>2) { $output = implode('', $str_output); $this->output($output); } } else { if (!$hideDisplay) { echo '' . "\n"; echo '' . "\n"; echo ''; } } if (!$hideDisplay) { if (serendipity_db_bool($this->get_config('followme_link', false))) { echo '

' . PLUGIN_TWITTER_FOLLOWME_LINK_TEXT . '

' . "\n"; } if ($service == 'twitter.com' && serendipity_db_bool($this->get_config('followme_widget', false))) { $extra_style = ''; if (serendipity_db_bool($this->get_config('followme_widget_dark', false))) { $extra_style .= ' data-button="grey" data-text-color="#FFFFFF" data-link-color="#00AEFF"'; } if (!serendipity_db_bool($this->get_config('followme_widget_counter', true))) { $extra_style .= ' data-show-count="false"'; } echo 'Follow @'.$username.''; } } if ($showformat == 'PHP') { // If the twitter event plugin is installed, too, save cache file in background. // When twitter is blocking, the blog isn't when using this background caching. // Background caching is done by a external plugin call, that is executed by the event plugin if (class_exists('serendipity_event_twitter')) { // add png that reloads the cache: $pluginurl = $serendipity['baseURL'] . $serendipity['indexFile'] . '?/' . $this->getPermaPluginPath(); $png_url = $pluginurl . '/cacheplugintwitter' .$this->cache_img_link_pars(); echo ''; } } if (serendipity_db_bool($this->get_config('backup')) && $service == 'twitter.com') { $last_backup = $this->get_config('last_backup', 0); if (date('Ymd') == date('Ymd', $last_backup)) { return true; } $this->checkTable(); $this->makeBackup($username, $last_backup); } } function cache_img_link_pars() { $service = $this->get_config('service'); $username = str_replace("_","!", $this->get_config('username')); $number = $this->get_config('number'); if (serendipity_db_bool($this->get_config('toall_only', false))) { $number = 50; // Fetch many in the hope, that there are enough globals with it. } $cachetime = $this->get_config('cachetime', 300); $suffix = "_{$service}_{$username}_{$number}_{$cachetime}"; if ($this->get_config('twitter_api', '1.0') == '1.1') { $idx = $this->get_config('twitter_oauth',0,FALSE); if (!empty($idx)) { $suffix = "{$suffix}_{$idx}_" . md5($this->pluginSecret() . "_{$idx}"); } // This is relevant only for API 1.1 if (!serendipity_db_bool($this->get_config('filter_rt', true))) { $suffix = "{$suffix}_r"; } } return $suffix; } function updateTwitterTimelineCache($cachefile){ global $serendipity; $cachetime = (int)$this->get_config('cachetime', 300); if (!file_exists($cachefile) || filemtime($cachefile) < (time()-$cachetime)) { $service = $this->get_config('service'); $username = $this->get_config('username'); $number = $this->get_config('number'); if (serendipity_db_bool($this->get_config('toall_only', false))) { $number = 50; // Fetch many in the hope, that there are enough globals with it. } if ($service == 'identi.ca') { $service_url = 'http://identi.ca/api'; $status_url = 'http://identi.ca/notice/'; $search_twitter_uri = $service_url . '/statuses/user_timeline/' . $username . '.json?count=' . $number; } else { $followme_url = 'https://twitter.com/' . $username; $service_url = 'https://api.twitter.com'; $status_url = 'https://twitter.com/' . $username . '/statuses/'; $search_twitter_uri = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $username . '&count=' . $number; } if (function_exists('serendipity_request_url')) { $response = serendipity_request_url($search_twitter_uri); $error = $serendipity['last_http_request']['responseCode']; } else { require_once S9Y_PEAR_PATH . 'HTTP/Request.php'; serendipity_request_start(); $req = new HTTP_Request($search_twitter_uri); $req->sendRequest(); $response = trim($req->getResponseBody()); $error = $req->getResponseCode(); serendipity_request_end(); } if ($error==200 &&!empty($response)) { $fp = fopen($cachefile, 'w'); fwrite($fp, serialize($response)); fflush($fp); fclose($fp); } } } /** * Return binary response for an image */ function show_img($filename, $mime_type='image/png') { header("Content-type: $mime_type"); header("Date: " . date("D, d M Y H:i:s T")); $fp = @fopen($filename, "rb"); if ($fp) { header('X-TwitterPluginPng: Found'); $filemtime = filemtime($filename); header("Content-Length: ". filesize($filename), true); header("Last-Modified: " . date("D, d M Y H:i:s T", $filemtime), true); fpassthru($fp); fclose($fp); } else { header('X-TwitterPluginPng: No-Image'); header("Content-Length: 0", true); header("Last-Modified: " . date("D, d M Y H:i:s T"), true); } return true; } function checkTable() { global $serendipity; $q = "CREATE TABLE IF NOT EXISTS {$serendipity['dbPrefix']}tweets ( id bigint(11) {PRIMARY}, tweetdate int(11), tweet longtext, username varchar(255), reply_to_status int(11), reply_to_user int(11), source varchar(255) );"; serendipity_db_schema_import($q); $db_version = $this->get_config("db_version"); // Convert tweet id to bigint! if (empty($db_version)) { $q = "ALTER TABLE {$serendipity['dbPrefix']}tweets CHANGE id id bigint(11)"; serendipity_db_schema_import($q); } $this->set_config("db_version",1); } function debugOut($string) { global $serendipity; $fp = fopen($serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . '/twitter.log', 'a'); fwrite($fp, date('Y-m-d H:i') . ' ' . $string . "\n"); fclose($fp); echo $string . "
\n"; } function twitterGet($url) { global $serendipity; $page = 1; // Twitter starts with page 1! $has_more = true; $last_tweetid = 0; $failsafe = 50; // Maximum of pages. I don't think it should ever get this high. while ($has_more) { if ($this->debug) $this->debugOut("Getting {$url}{$page}."); $out = file_get_contents($url . $page); $page++; $current_count = 0; if (empty($out) || $page > $failsafe) { if ($this->debug) $this->debugOut("No more results! (Failsafe?)"); $has_more = false; } $data = json_decode($out); if (!is_array($data)) { if ($this->debug) $this->debugOut("No result set."); } else { foreach($data AS $twitter_obj) { if (!is_object($twitter_obj) || empty($twitter_obj->id)) continue; $current_count++; $twitter_db = array( 'id' => $twitter_obj->id, 'tweetdate' => strtotime($twitter_obj->created_at), 'tweet' => $twitter_obj->text, 'reply_to_status' => $twitter_obj->in_reply_to_status_id, 'reply_to_user' => $twitter_obj->in_reply_to_user_id, 'username' => $twitter_obj->user->name, 'source' => $twitter_obj->source ); if ($last_tweetid == 0) { $last_tweetid = $twitter_obj->id; } $db_result = serendipity_db_insert('tweets', $twitter_db); if ($this->debug) $this->debugOut("Got #$current_count: " . substr($twitter_obj->text, 0, 15) . " dbresult:$db_result"); } } if ($current_count < 100) { if ($this->debug) $this->debugOut("No more pages."); $has_more = false; } } if ($this->debug) $this->debugOut("Storing last tweet: {$last_tweetid}"); if ($last_tweetid > 0) { $this->set_config('last_tweetid', $last_tweetid); } } function makeBackup($username, $last_backup) { global $serendipity; $this->set_config('last_backup', time()); if ($last_backup < 1) { // First time backup. Grab everything we can get. $this->twitterGet('https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $username . '&count=100&page='); } else { $this->twitterGet('https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $username . '&count=100&since_id=' . $this->get_config('last_tweetid') . '&page='); } } function makeDate($created_at,$dateformat) { if (serendipity_db_bool($this->get_config('use_time_ago'))) { return Twitter::create_status_ago_string($created_at); } $old_date = explode(" ", $created_at); $old_time = explode(":", $old_date[3]); switch($old_date[1]) { case 'Jan': $old_date[1]=1; break; case 'Feb': $old_date[1]=2; break; case 'Mar': $old_date[1]=3; break; case 'Apr': $old_date[1]=4; break; case 'May': $old_date[1]=5; break; case 'Jun': $old_date[1]=6; break; case 'Jul': $old_date[1]=7; break; case 'Aug': $old_date[1]=8; break; case 'Sep': $old_date[1]=9; break; case 'Oct': $old_date[1]=10; break; case 'Nov': $old_date[1]=11; break; case 'Dec': $old_date[1]=12; break; } $timestamp = mktime($old_time[0],$old_time[1],$old_time[2],$old_date[1],$old_date[2],$old_date[5]); if (LANG_CHARSET == 'UTF-8') { return serendipity_strftime($dateformat, $timestamp); } else { return utf8_encode(serendipity_strftime($dateformat, $timestamp)); } } function getPermaPluginPath() { global $serendipity; // Get configured plugin path: $pluginPath = 'plugin'; if (isset($serendipity['permalinkPluginPath'])){ $pluginPath = $serendipity['permalinkPluginPath']; } return $pluginPath; } function pluginSecret() { return serendipity_event_twitter::pluginSecret(); } }