356 lines
9.2 KiB
PHP
356 lines
9.2 KiB
PHP
|
<?php
|
||
|
|
||
|
require_once dirname(__FILE__) . '/OAuth.php';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Twitter for PHP - library for sending messages to Twitter and receiving status updates.
|
||
|
*
|
||
|
* @author David Grudl
|
||
|
* @copyright Copyright (c) 2008 David Grudl
|
||
|
* @license New BSD License
|
||
|
* @link http://phpfashion.com/
|
||
|
* @see http://dev.twitter.com/doc
|
||
|
* @version 3.2
|
||
|
*/
|
||
|
class Twitter
|
||
|
{
|
||
|
const API_URL = 'https://api.twitter.com/1.1/';
|
||
|
|
||
|
/**#@+ Timeline {@link Twitter::load()} */
|
||
|
const ME = 1;
|
||
|
const ME_AND_FRIENDS = 2;
|
||
|
const REPLIES = 3;
|
||
|
const RETWEETS = 128; // include retweets?
|
||
|
/**#@-*/
|
||
|
|
||
|
/** @var int */
|
||
|
public static $cacheExpire = 1800; // 30 min
|
||
|
|
||
|
/** @var string */
|
||
|
public static $cacheDir;
|
||
|
|
||
|
/** @var array */
|
||
|
public $httpOptions = array(
|
||
|
CURLOPT_TIMEOUT => 20,
|
||
|
CURLOPT_SSL_VERIFYPEER => 0,
|
||
|
CURLOPT_HTTPHEADER => array('Expect:'),
|
||
|
CURLOPT_USERAGENT => 'Twitter for PHP',
|
||
|
);
|
||
|
|
||
|
/** @var Twitter_OAuthSignatureMethod */
|
||
|
private $signatureMethod;
|
||
|
|
||
|
/** @var Twitter_OAuthConsumer */
|
||
|
private $consumer;
|
||
|
|
||
|
/** @var Twitter_OAuthConsumer */
|
||
|
private $token;
|
||
|
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Creates object using consumer and access keys.
|
||
|
* @param string consumer key
|
||
|
* @param string app secret
|
||
|
* @param string optional access token
|
||
|
* @param string optinal access token secret
|
||
|
* @throws TwitterException when CURL extension is not loaded
|
||
|
*/
|
||
|
public function __construct($consumerKey, $consumerSecret, $accessToken = NULL, $accessTokenSecret = NULL)
|
||
|
{
|
||
|
if (!extension_loaded('curl')) {
|
||
|
throw new TwitterException('PHP extension CURL is not loaded.');
|
||
|
}
|
||
|
|
||
|
$this->signatureMethod = new Twitter_OAuthSignatureMethod_HMAC_SHA1();
|
||
|
$this->consumer = new Twitter_OAuthConsumer($consumerKey, $consumerSecret);
|
||
|
$this->token = new Twitter_OAuthConsumer($accessToken, $accessTokenSecret);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Tests if user credentials are valid.
|
||
|
* @return boolean
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function authenticate()
|
||
|
{
|
||
|
try {
|
||
|
$res = $this->request('account/verify_credentials', 'GET');
|
||
|
return !empty($res->id);
|
||
|
|
||
|
} catch (TwitterException $e) {
|
||
|
if ($e->getCode() === 401) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Sends message to the Twitter.
|
||
|
* @param string message encoded in UTF-8
|
||
|
* @return object
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function send($message)
|
||
|
{
|
||
|
return $this->request('statuses/update', 'POST', array('status' => $message));
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns the most recent statuses.
|
||
|
* @param int timeline (ME | ME_AND_FRIENDS | REPLIES) and optional (RETWEETS)
|
||
|
* @param int number of statuses to retrieve
|
||
|
* @param int page of results to retrieve
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function load($flags = self::ME, $count = 20, array $data = NULL)
|
||
|
{
|
||
|
static $timelines = array(self::ME => 'user_timeline', self::ME_AND_FRIENDS => 'home_timeline', self::REPLIES => 'mentions_timeline');
|
||
|
if (!isset($timelines[$flags & 3])) {
|
||
|
throw new InvalidArgumentException;
|
||
|
}
|
||
|
|
||
|
return $this->cachedRequest('statuses/' . $timelines[$flags & 3], (array) $data + array(
|
||
|
'count' => $count,
|
||
|
'include_rts' => $flags & self::RETWEETS ? 1 : 0,
|
||
|
));
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns information of a given user.
|
||
|
* @param string name
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function loadUserInfo($user)
|
||
|
{
|
||
|
return $this->cachedRequest('users/show', array('screen_name' => $user));
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns information of a given user by id.
|
||
|
* @param string name
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function loadUserInfoById($id)
|
||
|
{
|
||
|
return $this->cachedRequest('users/show', array('user_id' => $id));
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns followers of a given user.
|
||
|
* @param string name
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function loadUserFollowers($user, $count = 5000, $cursor = -1, $cacheExpiry = null)
|
||
|
{
|
||
|
return $this->cachedRequest('followers/ids', array('screen_name' => $user, 'count' => $count, 'cursor' => $cursor), $cacheExpiry);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Destroys status.
|
||
|
* @param int id of status to be destroyed
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function destroy($id)
|
||
|
{
|
||
|
$res = $this->request("statuses/destroy/$id", 'POST');
|
||
|
return $res->id ? $res->id : FALSE;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns tweets that match a specified query.
|
||
|
* @param string|array query
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function search($query)
|
||
|
{
|
||
|
return $this->request('search/tweets', 'GET', is_array($query) ? $query : array('q' => $query))->statuses;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Process HTTP request.
|
||
|
* @param string URL or twitter command
|
||
|
* @param string HTTP method GET or POST
|
||
|
* @param array data
|
||
|
* @return mixed
|
||
|
* @throws TwitterException
|
||
|
*/
|
||
|
public function request($resource, $method, array $data = NULL)
|
||
|
{
|
||
|
if (!strpos($resource, '://')) {
|
||
|
if (!strpos($resource, '.')) {
|
||
|
$resource .= '.json';
|
||
|
}
|
||
|
$resource = self::API_URL . $resource;
|
||
|
}
|
||
|
|
||
|
foreach (array_keys((array) $data, NULL, TRUE) as $key) {
|
||
|
unset($data[$key]);
|
||
|
}
|
||
|
|
||
|
$request = Twitter_OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $resource, $data);
|
||
|
$request->sign_request($this->signatureMethod, $this->consumer, $this->token);
|
||
|
|
||
|
$options = array(
|
||
|
CURLOPT_HEADER => FALSE,
|
||
|
CURLOPT_RETURNTRANSFER => TRUE,
|
||
|
) + ($method === 'POST' ? array(
|
||
|
CURLOPT_POST => TRUE,
|
||
|
CURLOPT_POSTFIELDS => $request->to_postdata(),
|
||
|
CURLOPT_URL => $request->get_normalized_http_url(),
|
||
|
) : array(
|
||
|
CURLOPT_URL => $request->to_url(),
|
||
|
)) + $this->httpOptions;
|
||
|
|
||
|
$curl = curl_init();
|
||
|
curl_setopt_array($curl, $options);
|
||
|
$result = curl_exec($curl);
|
||
|
if (curl_errno($curl)) {
|
||
|
throw new TwitterException('Server error: ' . curl_error($curl));
|
||
|
}
|
||
|
|
||
|
$payload = version_compare(PHP_VERSION, '5.4.0') >= 0 ?
|
||
|
@json_decode($result, FALSE, 128, JSON_BIGINT_AS_STRING) : @json_decode($result); // intentionally @
|
||
|
|
||
|
if ($payload === FALSE) {
|
||
|
throw new TwitterException('Invalid server response');
|
||
|
}
|
||
|
|
||
|
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
|
if ($code >= 400) {
|
||
|
throw new TwitterException(isset($payload->errors[0]->message) ? $payload->errors[0]->message : "Server error #$code", $code);
|
||
|
}
|
||
|
|
||
|
return $payload;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Cached HTTP request.
|
||
|
* @param string URL or twitter command
|
||
|
* @param array
|
||
|
* @param int
|
||
|
* @return mixed
|
||
|
*/
|
||
|
public function cachedRequest($resource, array $data = NULL, $cacheExpire = NULL)
|
||
|
{
|
||
|
if (!self::$cacheDir) {
|
||
|
return $this->request($resource, 'GET', $data);
|
||
|
}
|
||
|
if ($cacheExpire === NULL) {
|
||
|
$cacheExpire = self::$cacheExpire;
|
||
|
}
|
||
|
|
||
|
$cacheFile = self::$cacheDir . '/twitter.' . md5($resource . json_encode($data) . serialize(array($this->consumer, $this->token)));
|
||
|
$cache = @json_decode(@file_get_contents($cacheFile)); // intentionally @
|
||
|
if ($cache && @filemtime($cacheFile) + $cacheExpire > time()) { // intentionally @
|
||
|
return $cache;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$payload = $this->request($resource, 'GET', $data);
|
||
|
file_put_contents($cacheFile, json_encode($payload));
|
||
|
return $payload;
|
||
|
|
||
|
} catch (TwitterException $e) {
|
||
|
if ($cache) {
|
||
|
return $cache;
|
||
|
}
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Makes twitter links, @usernames and #hashtags clickable.
|
||
|
* @param stdClass|string status
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function clickable($status)
|
||
|
{
|
||
|
if (!is_object($status)) { // back compatibility
|
||
|
trigger_error(__METHOD__ . '() has been changed; pass as parameter status object, not just text.', E_USER_WARNING);
|
||
|
return preg_replace_callback(
|
||
|
'~(?<!\w)(https?://\S+\w|www\.\S+\w|@\w+|#\w+)|[<>&]~u',
|
||
|
array(__CLASS__, 'clickableCallback'),
|
||
|
html_entity_decode($status, ENT_QUOTES, 'UTF-8')
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$all = array();
|
||
|
foreach ($status->entities->hashtags as $item) {
|
||
|
$all[$item->indices[0]] = array("http://twitter.com/search?q=%23$item->text", "#$item->text", $item->indices[1]);
|
||
|
}
|
||
|
foreach ($status->entities->urls as $item) {
|
||
|
if (!isset($item->expanded_url)) {
|
||
|
$all[$item->indices[0]] = array($item->url, $item->url, $item->indices[1]);
|
||
|
} else {
|
||
|
$all[$item->indices[0]] = array($item->expanded_url, $item->display_url, $item->indices[1]);
|
||
|
}
|
||
|
}
|
||
|
foreach ($status->entities->user_mentions as $item) {
|
||
|
$all[$item->indices[0]] = array("http://twitter.com/$item->screen_name", "@$item->screen_name", $item->indices[1]);
|
||
|
}
|
||
|
if (isset($status->entities->media)) {
|
||
|
foreach ($status->entities->media as $item) {
|
||
|
$all[$item->indices[0]] = array($item->url, $item->display_url, $item->indices[1]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
krsort($all);
|
||
|
$s = $status->text;
|
||
|
foreach ($all as $pos => $item) {
|
||
|
$s = iconv_substr($s, 0, $pos, 'UTF-8')
|
||
|
. '<a href="' . htmlspecialchars($item[0]) . '">' . htmlspecialchars($item[1]) . '</a>'
|
||
|
. iconv_substr($s, $item[2], iconv_strlen($s, 'UTF-8'), 'UTF-8');
|
||
|
}
|
||
|
return $s;
|
||
|
}
|
||
|
|
||
|
|
||
|
private static function clickableCallback($m)
|
||
|
{
|
||
|
$m = htmlspecialchars($m[0]);
|
||
|
if ($m[0] === '#') {
|
||
|
$m = substr($m, 1);
|
||
|
return "<a href='http://twitter.com/search?q=%23$m'>#$m</a>";
|
||
|
} elseif ($m[0] === '@') {
|
||
|
$m = substr($m, 1);
|
||
|
return "@<a href='http://twitter.com/$m'>$m</a>";
|
||
|
} elseif ($m[0] === 'w') {
|
||
|
return "<a href='http://$m'>$m</a>";
|
||
|
} elseif ($m[0] === 'h') {
|
||
|
return "<a href='$m'>$m</a>";
|
||
|
} else {
|
||
|
return $m;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
/**
|
||
|
* An exception generated by Twitter.
|
||
|
*/
|
||
|
class TwitterException extends Exception
|
||
|
{
|
||
|
}
|