]> git.mxchange.org Git - friendica-addons.git/blob - appnet/AppDotNet.php
Additional work for PR 3778
[friendica-addons.git] / appnet / AppDotNet.php
1 <?php
2 /**
3  * AppDotNet.php
4  * App.net PHP library
5  * https://github.com/jdolitsky/AppDotNetPHP
6  *
7  * This class handles a lower level type of access to App.net. It's ideal
8  * for command line scripts and other places where you want full control
9  * over what's happening, and you're at least a little familiar with oAuth.
10  *
11  * Alternatively you can use the EZAppDotNet class which automatically takes
12  * care of a lot of the details like logging in, keeping track of tokens,
13  * etc. EZAppDotNet assumes you're accessing App.net via a browser, whereas
14  * this class tries to make no assumptions at all.
15  */
16 class AppDotNet {
17
18         protected $_baseUrl = 'https://alpha-api.app.net/stream/0/';
19         protected $_authUrl = 'https://account.app.net/oauth/';
20
21         private $_authPostParams=array();
22
23         // stores the access token after login
24         private $_accessToken = null;
25
26         // stores the App access token if we have it
27         private $_appAccessToken = null;
28
29         // stores the user ID returned when fetching the auth token
30         private $_user_id = null;
31
32         // stores the username returned when fetching the auth token
33         private $_username = null;
34
35         // The total number of requests you're allowed within the alloted time period
36         private $_rateLimit = null;
37
38         // The number of requests you have remaining within the alloted time period
39         private $_rateLimitRemaining = null;
40
41         // The number of seconds remaining in the alloted time period
42         private $_rateLimitReset = null;
43
44         // The scope the user has
45         private $_scope = null;
46
47         // token scopes
48         private $_scopes=array();
49
50         // debug info
51         private $_last_request = null;
52         private $_last_response = null;
53
54         // ssl certification
55         private $_sslCA = null;
56
57         // the callback function to be called when an event is received from the stream
58         private $_streamCallback = null;
59
60         // the stream buffer
61         private $_streamBuffer = '';
62
63         // stores the curl handler for the current stream
64         private $_currentStream = null;
65
66         // stores the curl multi handler for the current stream
67         private $_multiStream = null;
68
69         // stores the number of failed connects, so we can back off multiple failures
70         private $_connectFailCounter = 0;
71
72         // stores the most recent stream url, so we can re-connect when needed
73         private $_streamUrl = null;
74
75         // keeps track of the last time we've received a packet from the api, if it's too long we'll reconnect
76         private $_lastStreamActivity = null;
77
78         // stores the headers received when connecting to the stream
79         private $_streamHeaders = null;
80
81         // response meta max_id data
82         private $_maxid = null;
83
84         // response meta min_id data
85         private $_minid = null;
86
87         // response meta more data
88         private $_more = null;
89
90         // response stream marker data
91         private $_last_marker = null;
92
93         // strip envelope response from returned value
94         private $_stripResponseEnvelope=true;
95
96         // if processing stream_markers or any fast stream, decrease $sleepFor
97         public $streamingSleepFor=20000;
98
99         /**
100          * Constructs an AppDotNet PHP object with the specified client ID and
101          * client secret.
102          * @param string $client_id The client ID you received from App.net when
103          * creating your app.
104          * @param string $client_secret The client secret you received from
105          * App.net when creating your app.
106          */
107         public function __construct($client_id,$client_secret) {
108                 $this->_clientId = $client_id;
109                 $this->_clientSecret = $client_secret;
110
111                 // if the digicert certificate exists in the same folder as this file,
112                 // remember that fact for later
113                 if (file_exists(dirname(__FILE__).'/DigiCertHighAssuranceEVRootCA.pem')) {
114                         $this->_sslCA = dirname(__FILE__).'/DigiCertHighAssuranceEVRootCA.pem';
115                 }
116         }
117
118         /**
119          * Set whether or not to strip Envelope Response (meta) information
120          * This option will be deprecated in the future. Is it to allow
121          * a stepped migration path between code expecting the old behavior
122          * and new behavior. When not stripped, you still can use the proper
123          * method to pull the meta information. Please start converting your code ASAP
124          */
125         public function includeResponseEnvelope() {
126                 $this->_stripResponseEnvelope=false;
127         }
128
129         /**
130          * Construct the proper Auth URL for the user to visit and either grant
131          * or not access to your app. Usually you would place this as a link for
132          * the user to client, or a redirect to send them to the auth URL.
133          * Also can be called after authentication for additional scopes
134          * @param string $callbackUri Where you want the user to be directed
135          * after authenticating with App.net. This must be one of the URIs
136          * allowed by your App.net application settings.
137          * @param array $scope An array of scopes (permissions) you wish to obtain
138          * from the user. Currently options are stream, email, write_post, follow,
139          * messages, and export. If you don't specify anything, you'll only receive
140          * access to the user's basic profile (the default).
141          */
142         public function getAuthUrl($callback_uri,$scope=null) {
143
144                 // construct an authorization url based on our client id and other data
145                 $data = array(
146                         'client_id'=>$this->_clientId,
147                         'response_type'=>'code',
148                         'redirect_uri'=>$callback_uri,
149                 );
150
151                 $url = $this->_authUrl;
152                 if ($this->_accessToken) {
153                         $url .= 'authorize?';
154                 } else {
155                         $url .= 'authenticate?';
156                 }
157                 $url .= $this->buildQueryString($data);
158
159                 if ($scope) {
160                         $url .= '&scope='.implode('+',$scope);
161                 }
162
163                 // return the constructed url
164                 return $url;
165         }
166
167         /**
168          * Call this after they return from the auth page, or anytime you need the
169          * token. For example, you could store it in a database and use
170          * setAccessToken() later on to return on behalf of the user.
171          */
172         public function getAccessToken($callback_uri) {
173                 // if there's no access token set, and they're returning from
174                 // the auth page with a code, use the code to get a token
175                 if (!$this->_accessToken && isset($_GET['code']) && $_GET['code']) {
176
177                         // construct the necessary elements to get a token
178                         $data = array(
179                                 'client_id'=>$this->_clientId,
180                                 'client_secret'=>$this->_clientSecret,
181                                 'grant_type'=>'authorization_code',
182                                 'redirect_uri'=>$callback_uri,
183                                 'code'=>$_GET['code']
184                         );
185
186                         // try and fetch the token with the above data
187                         $res = $this->httpReq('post',$this->_authUrl.'access_token', $data);
188
189                         // store it for later
190                         $this->_accessToken = $res['access_token'];
191                         $this->_username = $res['username'];
192                         $this->_user_id = $res['user_id'];
193                 }
194
195                 // return what we have (this may be a token, or it may be nothing)
196                 return $this->_accessToken;
197         }
198
199         /**
200          * Check the scope of current token to see if it has required scopes
201          * has to be done after a check
202          */
203         public function checkScopes($app_scopes) {
204                 if (!count($this->_scopes)) {
205                         return -1; // _scope is empty
206                 }
207                 $missing=array();
208                 foreach($app_scopes as $scope) {
209                         if (!in_array($scope,$this->_scopes)) {
210                                 if ($scope=='public_messages') {
211                                         // messages works for public_messages
212                                         if (in_array('messages',$this->_scopes)) {
213                                                 // if we have messages in our scopes
214                                                 continue;
215                                         }
216                                 }
217                                 $missing[]=$scope;
218                         }
219                 }
220                 // identify the ones missing
221                 if (count($missing)) {
222                         // do something
223                         return $missing;
224                 }
225                 return 0; // 0 missing
226          }
227
228         /**
229          * Set the access token (eg: after retrieving it from offline storage)
230          * @param string $token A valid access token you're previously received
231          * from calling getAccessToken().
232          */
233         public function setAccessToken($token) {
234                 $this->_accessToken = $token;
235         }
236
237         /**
238          * Deauthorize the current token (delete your authorization from the API)
239          * Generally this is useful for logging users out from a web app, so they
240          * don't get automatically logged back in the next time you redirect them
241          * to the authorization URL.
242          */
243         public function deauthorizeToken() {
244                 return $this->httpReq('delete',$this->_baseUrl.'token');
245         }
246
247         /**
248          * Retrieve an app access token from the app.net API. This allows you
249          * to access the API without going through the user access flow if you
250          * just want to (eg) consume global. App access tokens are required for
251          * some actions (like streaming global). DO NOT share the return value
252          * of this function with any user (or save it in a cookie, etc). This
253          * is considered secret info for your app only.
254          * @return string The app access token
255          */
256         public function getAppAccessToken() {
257
258                 // construct the necessary elements to get a token
259                 $data = array(
260                         'client_id'=>$this->_clientId,
261                         'client_secret'=>$this->_clientSecret,
262                         'grant_type'=>'client_credentials',
263                 );
264
265                 // try and fetch the token with the above data
266                 $res = $this->httpReq('post',$this->_authUrl.'access_token', $data);
267
268                 // store it for later
269                 $this->_appAccessToken = $res['access_token'];
270                 $this->_accessToken = $res['access_token'];
271                 $this->_username = null;
272                 $this->_user_id = null;
273
274                 return $this->_accessToken;
275         }
276
277         /**
278          * Returns the total number of requests you're allowed within the
279          * alloted time period.
280          * @see getRateLimitReset()
281          */
282         public function getRateLimit() {
283                 return $this->_rateLimit;
284         }
285
286         /**
287          * The number of requests you have remaining within the alloted time period
288          * @see getRateLimitReset()
289          */
290         public function getRateLimitRemaining() {
291                 return $this->_rateLimitRemaining;
292         }
293
294         /**
295          * The number of seconds remaining in the alloted time period.
296          * When this time is up you'll have getRateLimit() available again.
297          */
298         public function getRateLimitReset() {
299                 return $this->_rateLimitReset;
300         }
301
302         /**
303          * The scope the user has
304          */
305         public function getScope() {
306                 return $this->_scope;
307         }
308
309         /**
310          * Internal function, parses out important information App.net adds
311          * to the headers.
312          */
313         protected function parseHeaders($response) {
314                 // take out the headers
315                 // set internal variables
316                 // return the body/content
317                 $this->_rateLimit = null;
318                 $this->_rateLimitRemaining = null;
319                 $this->_rateLimitReset = null;
320                 $this->_scope = null;
321
322                 $response = explode("\r\n\r\n",$response,2);
323                 $headers = $response[0];
324
325                 if($headers == 'HTTP/1.1 100 Continue') {
326                         $response = explode("\r\n\r\n",$response[1],2);
327                         $headers = $response[0];
328                 }
329
330                 if (isset($response[1])) {
331                         $content = $response[1];
332                 }
333                 else {
334                         $content = null;
335                 }
336
337                 // this is not a good way to parse http headers
338                 // it will not (for example) take into account multiline headers
339                 // but what we're looking for is pretty basic, so we can ignore those shortcomings
340                 $headers = explode("\r\n",$headers);
341                 foreach ($headers as $header) {
342                         $header = explode(': ',$header,2);
343                         if (count($header)<2) {
344                                 continue;
345                         }
346                         list($k,$v) = $header;
347                         switch ($k) {
348                                 case 'X-RateLimit-Remaining':
349                                         $this->_rateLimitRemaining = $v;
350                                         break;
351                                 case 'X-RateLimit-Limit':
352                                         $this->_rateLimit = $v;
353                                         break;
354                                 case 'X-RateLimit-Reset':
355                                         $this->_rateLimitReset = $v;
356                                         break;
357                                 case 'X-OAuth-Scopes':
358                                         $this->_scope = $v;
359                                         $this->_scopes=explode(',',$v);
360                                         break;
361                         }
362                 }
363                 return $content;
364         }
365
366         /**
367          * Internal function. Used to turn things like TRUE into 1, and then
368          * calls http_build_query.
369          */
370         protected function buildQueryString($array) {
371                 foreach ($array as $k=>&$v) {
372                         if ($v===true) {
373                                 $v = '1';
374                         }
375                         elseif ($v===false) {
376                                 $v = '0';
377                         }
378                         unset($v);
379                 }
380                 return http_build_query($array);
381         }
382
383
384         /**
385          * Internal function to handle all
386          * HTTP requests (POST,PUT,GET,DELETE)
387          */
388         protected function httpReq($act, $req, $params=array(),$contentType='application/x-www-form-urlencoded') {
389                 $ch = curl_init($req);
390                 $headers = array();
391                 if($act != 'get') {
392                         curl_setopt($ch, CURLOPT_POST, true);
393                         // if they passed an array, build a list of parameters from it
394                         if (is_array($params) && $act != 'post-raw') {
395                                 $params = $this->buildQueryString($params);
396                         }
397                         curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
398                         $headers[] = "Content-Type: ".$contentType;
399                 }
400                 if($act != 'post' && $act != 'post-raw') {
401                         curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($act));
402                 }
403                 if($act == 'get' && isset($params['access_token'])) {
404                         $headers[] = 'Authorization: Bearer '.$params['access_token'];
405                 }
406                 else if ($this->_accessToken) {
407                         $headers[] = 'Authorization: Bearer '.$this->_accessToken;
408                 }
409                 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
410                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
411                 curl_setopt($ch, CURLINFO_HEADER_OUT, true);
412                 curl_setopt($ch, CURLOPT_HEADER, true);
413                 if ($this->_sslCA) {
414                         curl_setopt($ch, CURLOPT_CAINFO, $this->_sslCA);
415                 }
416                 $this->_last_response = curl_exec($ch);
417                 $this->_last_request = curl_getinfo($ch,CURLINFO_HEADER_OUT);
418                 $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
419                 curl_close($ch);
420                 if ($http_status==0) {
421                         throw new AppDotNetException('Unable to connect to '.$req);
422                 }
423                 if ($http_status<200 || $http_status>=300) {
424                         throw new AppDotNetException('HTTP error '.$this->_last_response);
425                 }
426                 if ($this->_last_request===false) {
427                         if (!curl_getinfo($ch,CURLINFO_SSL_VERIFYRESULT)) {
428                                 throw new AppDotNetException('SSL verification failed, connection terminated.');
429                         }
430                 }
431                 $response = $this->parseHeaders($this->_last_response);
432                 $response = json_decode($response,true);
433
434                 if (isset($response['meta'])) {
435                         if (isset($response['meta']['max_id'])) {
436                                 $this->_maxid=$response['meta']['max_id'];
437                                 $this->_minid=$response['meta']['min_id'];
438                         }
439                         if (isset($response['meta']['more'])) {
440                                 $this->_more=$response['meta']['more'];
441                         }
442                         if (isset($response['meta']['marker'])) {
443                                 $this->_last_marker=$response['meta']['marker'];
444                         }
445                 }
446
447                 // look for errors
448                 if (isset($response['error'])) {
449                         if (is_array($response['error'])) {
450                                 throw new AppDotNetException($response['error']['message'],
451                                                                 $response['error']['code']);
452                         }
453                         else {
454                                 throw new AppDotNetException($response['error']);
455                         }
456                 }
457
458                 // look for response migration errors
459                 elseif (isset($response['meta']) && isset($response['meta']['error_message'])) {
460                         throw new AppDotNetException($response['meta']['error_message'],$response['meta']['code']);
461                 }
462
463                 // if we've received a migration response, handle it and return data only
464                 elseif ($this->_stripResponseEnvelope && isset($response['meta']) && isset($response['data'])) {
465                         return $response['data'];
466                 }
467
468                 // else non response migration response, just return it
469                 else {
470                         return $response;
471                 }
472         }
473
474
475         /**
476          * Get max_id from last meta response data envelope
477          */
478         public function getResponseMaxID() {
479                 return $this->_maxid;
480         }
481
482         /**
483          * Get min_id from last meta response data envelope
484          */
485         public function getResponseMinID() {
486                 return $this->_minid;
487         }
488
489         /**
490          * Get more from last meta response data envelope
491          */
492         public function getResponseMore() {
493                 return $this->_more;
494         }
495
496         /**
497          * Get marker from last meta response data envelope
498          */
499         public function getResponseMarker() {
500                 return $this->_last_marker;
501         }
502
503         /**
504          * Fetch API configuration object
505          */
506         public function getConfig() {
507                 return $this->httpReq('get',$this->_baseUrl.'config');
508         }
509
510         /**
511          * Return the Filters for the current user.
512          */
513         public function getAllFilters() {
514                 return $this->httpReq('get',$this->_baseUrl.'filters');
515         }
516
517         /**
518          * Create a Filter for the current user.
519          * @param string $name The name of the new filter
520          * @param array $filters An associative array of filters to be applied.
521          * This may change as the API evolves, as of this writing possible
522          * values are: user_ids, hashtags, link_domains, and mention_user_ids.
523          * You will need to provide at least one filter name=>value pair.
524          */
525         public function createFilter($name='New filter', $filters=array()) {
526                 $filters['name'] = $name;
527                 return $this->httpReq('post',$this->_baseUrl.'filters',$filters);
528         }
529
530         /**
531          * Returns a specific Filter object.
532          * @param integer $filter_id The ID of the filter you wish to retrieve.
533          */
534         public function getFilter($filter_id=null) {
535                 return $this->httpReq('get',$this->_baseUrl.'filters/'.urlencode($filter_id));
536         }
537
538         /**
539          * Delete a Filter. The Filter must belong to the current User.
540          * @return object Returns the deleted Filter on success.
541          */
542         public function deleteFilter($filter_id=null) {
543                 return $this->httpReq('delete',$this->_baseUrl.'filters/'.urlencode($filter_id));
544         }
545
546         /**
547          * Process user description, message or post text.
548          * Mentions and hashtags will be parsed out of the
549          * text, as will bare URLs. To create a link in the text without using a
550          * bare URL, include the anchor text in the object text and include a link
551          * entity in the function call.
552          * @param string $text The text of the description/message/post
553          * @param array $data An associative array of optional post data. This
554          * will likely change as the API evolves, as of this writing allowed keys are:
555          * reply_to, and annotations. "annotations" may be a complex object represented
556          * by an associative array.
557          * @param array $params An associative array of optional data to be included
558          * in the URL (such as 'include_annotations' and 'include_machine')
559          * @return array An associative array representing the post.
560          */
561         public function processText($text=null, $data = array(), $params = array()) {
562                 $data['text'] = $text;
563                 $json = json_encode($data);
564                 $qs = '';
565                 if (!empty($params)) {
566                         $qs = '?'.$this->buildQueryString($params);
567                 }
568                 return $this->httpReq('post',$this->_baseUrl.'text/process'.$qs, $json, 'application/json');
569         }
570
571         /**
572          * Create a new Post object. Mentions and hashtags will be parsed out of the
573          * post text, as will bare URLs. To create a link in a post without using a
574          * bare URL, include the anchor text in the post's text and include a link
575          * entity in the post creation call.
576          * @param string $text The text of the post
577          * @param array $data An associative array of optional post data. This
578          * will likely change as the API evolves, as of this writing allowed keys are:
579          * reply_to, and annotations. "annotations" may be a complex object represented
580          * by an associative array.
581          * @param array $params An associative array of optional data to be included
582          * in the URL (such as 'include_annotations' and 'include_machine')
583          * @return array An associative array representing the post.
584          */
585         public function createPost($text=null, $data = array(), $params = array()) {
586                 $data['text'] = $text;
587
588                 $json = json_encode($data);
589                 $qs = '';
590                 if (!empty($params)) {
591                         $qs = '?'.$this->buildQueryString($params);
592                 }
593                 return $this->httpReq('post',$this->_baseUrl.'posts'.$qs, $json, 'application/json');
594         }
595
596         /**
597          * Returns a specific Post.
598          * @param integer $post_id The ID of the post to retrieve
599          * @param array $params An associative array of optional general parameters.
600          * This will likely change as the API evolves, as of this writing allowed keys
601          * are: include_annotations.
602          * @return array An associative array representing the post
603          */
604         public function getPost($post_id=null,$params = array()) {
605                 return $this->httpReq('get',$this->_baseUrl.'posts/'.urlencode($post_id)
606                                                 .'?'.$this->buildQueryString($params));
607         }
608
609         /**
610          * Delete a Post. The current user must be the same user who created the Post.
611          * It returns the deleted Post on success.
612          * @param integer $post_id The ID of the post to delete
613          * @param array An associative array representing the post that was deleted
614          */
615         public function deletePost($post_id=null) {
616                 return $this->httpReq('delete',$this->_baseUrl.'posts/'.urlencode($post_id));
617         }
618
619         /**
620          * Retrieve the Posts that are 'in reply to' a specific Post.
621          * @param integer $post_id The ID of the post you want to retrieve replies for.
622          * @param array $params An associative array of optional general parameters.
623          * This will likely change as the API evolves, as of this writing allowed keys
624          * are: count, before_id, since_id, include_muted, include_deleted,
625          * include_directed_posts, and include_annotations.
626          * @return An array of associative arrays, each representing a single post.
627          */
628         public function getPostReplies($post_id=null,$params = array()) {
629                 return $this->httpReq('get',$this->_baseUrl.'posts/'.urlencode($post_id)
630                                 .'/replies?'.$this->buildQueryString($params));
631         }
632
633         /**
634          * Get the most recent Posts created by a specific User in reverse
635          * chronological order (most recent first).
636          * @param mixed $user_id Either the ID of the user you wish to retrieve posts by,
637          * or the string "me", which will retrieve posts for the user you're authenticated
638          * as.
639          * @param array $params An associative array of optional general parameters.
640          * This will likely change as the API evolves, as of this writing allowed keys
641          * are: count, before_id, since_id, include_muted, include_deleted,
642          * include_directed_posts, and include_annotations.
643          * @return An array of associative arrays, each representing a single post.
644          */
645         public function getUserPosts($user_id='me', $params = array()) {
646                 return $this->httpReq('get',$this->_baseUrl.'users/'.urlencode($user_id)
647                                         .'/posts?'.$this->buildQueryString($params));
648         }
649
650         /**
651          * Get the most recent Posts mentioning by a specific User in reverse
652          * chronological order (newest first).
653          * @param mixed $user_id Either the ID of the user who is being mentioned, or
654          * the string "me", which will retrieve posts for the user you're authenticated
655          * as.
656          * @param array $params An associative array of optional general parameters.
657          * This will likely change as the API evolves, as of this writing allowed keys
658          * are: count, before_id, since_id, include_muted, include_deleted,
659          * include_directed_posts, and include_annotations.
660          * @return An array of associative arrays, each representing a single post.
661          */
662         public function getUserMentions($user_id='me',$params = array()) {
663                 return $this->httpReq('get',$this->_baseUrl.'users/'
664                         .urlencode($user_id).'/mentions?'.$this->buildQueryString($params));
665         }
666
667         /**
668          * Return the 20 most recent posts from the current User and
669          * the Users they follow.
670          * @param array $params An associative array of optional general parameters.
671          * This will likely change as the API evolves, as of this writing allowed keys
672          * are: count, before_id, since_id, include_muted, include_deleted,
673          * include_directed_posts, and include_annotations.
674          * @return An array of associative arrays, each representing a single post.
675          */
676         public function getUserStream($params = array()) {
677                 return $this->httpReq('get',$this->_baseUrl.'posts/stream?'.$this->buildQueryString($params));
678         }
679
680         /**
681          * Returns a specific user object.
682          * @param mixed $user_id The ID of the user you want to retrieve, or the string
683          * "me" to retrieve data for the users you're currently authenticated as.
684          * @param array $params An associative array of optional general parameters.
685          * This will likely change as the API evolves, as of this writing allowed keys
686          * are: include_annotations|include_user_annotations.
687          * @return array An associative array representing the user data.
688          */
689         public function getUser($user_id='me', $params = array()) {
690                 return $this->httpReq('get',$this->_baseUrl.'users/'.urlencode($user_id)
691                                                 .'?'.$this->buildQueryString($params));
692         }
693
694         /**
695          * Returns multiple users request by an array of user ids
696          * @param array $params An associative array of optional general parameters.
697          * This will likely change as the API evolves, as of this writing allowed keys
698          * are: include_annotations|include_user_annotations.
699          * @return array An associative array representing the users data.
700          */
701         public function getUsers($user_arr, $params = array()) {
702                 return $this->httpReq('get',$this->_baseUrl.'users?ids='.join(',',$user_arr)
703                                         .'&'.$this->buildQueryString($params));
704         }
705
706         /**
707          * Add the specified user ID to the list of users followed.
708          * Returns the User object of the user being followed.
709          * @param integer $user_id The user ID of the user to follow.
710          * @return array An associative array representing the user you just followed.
711          */
712         public function followUser($user_id=null) {
713                 return $this->httpReq('post',$this->_baseUrl.'users/'.urlencode($user_id).'/follow');
714         }
715
716         /**
717          * Removes the specified user ID to the list of users followed.
718          * Returns the User object of the user being unfollowed.
719          * @param integer $user_id The user ID of the user to unfollow.
720          * @return array An associative array representing the user you just unfollowed.
721          */
722         public function unfollowUser($user_id=null) {
723                 return $this->httpReq('delete',$this->_baseUrl.'users/'.urlencode($user_id).'/follow');
724         }
725
726         /**
727          * Returns an array of User objects the specified user is following.
728          * @param mixed $user_id Either the ID of the user being followed, or
729          * the string "me", which will retrieve posts for the user you're authenticated
730          * as.
731          * @return array An array of associative arrays, each representing a single
732          * user following $user_id
733          */
734         public function getFollowing($user_id='me') {
735                 return $this->httpReq('get',$this->_baseUrl.'users/'.$user_id.'/following');
736         }
737
738         /**
739          * Returns an array of User objects for users following the specified user.
740          * @param mixed $user_id Either the ID of the user being followed, or
741          * the string "me", which will retrieve posts for the user you're authenticated
742          * as.
743          * @return array An array of associative arrays, each representing a single
744          * user following $user_id
745          */
746         public function getFollowers($user_id='me') {
747                 return $this->httpReq('get',$this->_baseUrl.'users/'.$user_id.'/followers');
748         }
749
750         /**
751          * Return Posts matching a specific #hashtag.
752          * @param string $hashtag The hashtag you're looking for.
753          * @param array $params An associative array of optional general parameters.
754          * This will likely change as the API evolves, as of this writing allowed keys
755          * are: count, before_id, since_id, include_muted, include_deleted,
756          * include_directed_posts, and include_annotations.
757          * @return An array of associative arrays, each representing a single post.
758          */
759         public function searchHashtags($hashtag=null, $params = array()) {
760                 return $this->httpReq('get',$this->_baseUrl.'posts/tag/'
761                                 .urlencode($hashtag).'?'.$this->buildQueryString($params));
762         }
763
764         /**
765          * Retrieve a list of all public Posts on App.net, often referred to as the
766          * global stream.
767          * @param array $params An associative array of optional general parameters.
768          * This will likely change as the API evolves, as of this writing allowed keys
769          * are: count, before_id, since_id, include_muted, include_deleted,
770          * include_directed_posts, and include_annotations.
771          * @return An array of associative arrays, each representing a single post.
772          */
773         public function getPublicPosts($params = array()) {
774                 return $this->httpReq('get',$this->_baseUrl.'posts/stream/global?'.$this->buildQueryString($params));
775         }
776
777         /**
778          * List User interactions
779          */
780         public function getMyInteractions($params = array()) {
781                 return $this->httpReq('get',$this->_baseUrl.'users/me/interactions?'.$this->buildQueryString($params));
782         }
783
784         /**
785          * Retrieve a user's user ID by specifying their username.
786          * Now supported by the API. We use the API if we have a token
787          * Otherwise we scrape the alpha.app.net site for the info.
788          * @param string $username The username of the user you want the ID of, without
789          * an @ symbol at the beginning.
790          * @return integer The user's user ID
791          */
792         public function getIdByUsername($username=null) {
793                 if ($this->_accessToken) {
794                         $res=$this->httpReq('get',$this->_baseUrl.'users/@'.$username);
795                         $user_id=$res['data']['id'];
796                 } else {
797                         $ch = curl_init('https://alpha.app.net/'.urlencode(strtolower($username)));
798                         curl_setopt($ch, CURLOPT_POST, false);
799                         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
800                         curl_setopt($ch,CURLOPT_USERAGENT,
801                                 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1');
802                         $response = curl_exec($ch);
803                         curl_close($ch);
804                         $temp = explode('title="User Id ',$response);
805                         $temp2 = explode('"',$temp[1]);
806                         $user_id = $temp2[0];
807                 }
808                 return $user_id;
809         }
810
811         /**
812          * Mute a user
813          * @param integer $user_id The user ID to mute
814          */
815         public function muteUser($user_id=null) {
816                 return $this->httpReq('post',$this->_baseUrl.'users/'.urlencode($user_id).'/mute');
817         }
818
819         /**
820          * Unmute a user
821          * @param integer $user_id The user ID to unmute
822          */
823         public function unmuteUser($user_id=null) {
824                 return $this->httpReq('delete',$this->_baseUrl.'users/'.urlencode($user_id).'/mute');
825         }
826
827         /**
828          * List the users muted by the current user
829          * @return array An array of associative arrays, each representing one muted user.
830          */
831         public function getMuted() {
832                 return $this->httpReq('get',$this->_baseUrl.'users/me/muted');
833         }
834
835         /**
836         * Star a post
837         * @param integer $post_id The post ID to star
838         */
839         public function starPost($post_id=null) {
840                 return $this->httpReq('post',$this->_baseUrl.'posts/'.urlencode($post_id).'/star');
841         }
842
843         /**
844         * Unstar a post
845         * @param integer $post_id The post ID to unstar
846         */
847         public function unstarPost($post_id=null) {
848                 return $this->httpReq('delete',$this->_baseUrl.'posts/'.urlencode($post_id).'/star');
849         }
850
851         /**
852         * List the posts starred by the current user
853         * @param array $params An associative array of optional general parameters.
854         * This will likely change as the API evolves, as of this writing allowed keys
855         * are:  count, before_id, since_id, include_muted, include_deleted,
856         * include_directed_posts, and include_annotations.
857         * See https://github.com/appdotnet/api-spec/blob/master/resources/posts.md#general-parameters
858         * @return array An array of associative arrays, each representing a single
859         * user who has starred a post
860         */
861         public function getStarred($user_id='me', $params = array()) {
862                 return $this->httpReq('get',$this->_baseUrl.'users/'.urlencode($user_id).'/stars'
863                                         .'?'.$this->buildQueryString($params));
864         }
865
866         /**
867         * List the users who have starred a post
868         * @param integer $post_id the post ID to get stars from
869         * @return array An array of associative arrays, each representing one user.
870         */
871         public function getStars($post_id=null) {
872                 return $this->httpReq('get',$this->_baseUrl.'posts/'.urlencode($post_id).'/stars');
873         }
874
875         /**
876          * Returns an array of User objects of users who reposted the specified post.
877          * @param integer $post_id the post ID to
878          * @return array An array of associative arrays, each representing a single
879          * user who reposted $post_id
880          */
881         public function getReposters($post_id){
882                 return $this->httpReq('get',$this->_baseUrl.'posts/'.urlencode($post_id).'/reposters');
883         }
884
885         /**
886          * Repost an existing Post object.
887          * @param integer $post_id The id of the post
888          * @return not a clue
889          */
890         public function repost($post_id){
891                 return $this->httpReq('post',$this->_baseUrl.'posts/'.urlencode($post_id).'/repost');
892         }
893
894         /**
895          * Delete a post that the user has reposted.
896          * @param integer $post_id The id of the post
897          * @return not a clue
898          */
899         public function deleteRepost($post_id){
900                 return $this->httpReq('delete',$this->_baseUrl.'posts/'.urlencode($post_id).'/repost');
901         }
902
903         /**
904         * List the posts who match a specific search term
905         * @param array $params a list of filter, search query, and general Post parameters
906         * see: https://developers.app.net/reference/resources/post/search/
907         * @param string $query The search query. Supports
908         * normal search terms. Searches post text.
909         * @return array An array of associative arrays, each representing one post.
910         * or false on error
911         */
912         public function searchPosts($params = array(), $query='', $order='default') {
913                 if (!is_array($params)) {
914                         return false;
915                 }
916                 if (!empty($query)) {
917                         $params['query']=$query;
918                 }
919                 if ($order=='default') {
920                         if (!empty($query)) {
921                                 $params['order']='score';
922                         } else {
923                                 $params['order']='id';
924                 }
925                 }
926                 return $this->httpReq('get',$this->_baseUrl.'posts/search?'.$this->buildQueryString($params));
927         }
928
929
930         /**
931         * List the users who match a specific search term
932         * @param string $search The search query. Supports @username or #tag searches as
933         * well as normal search terms. Searches username, display name, bio information.
934         * Does not search posts.
935         * @return array An array of associative arrays, each representing one user.
936         */
937         public function searchUsers($search="") {
938                 return $this->httpReq('get',$this->_baseUrl.'users/search?q='.urlencode($search));
939         }
940
941         /**
942          * Return the 20 most recent posts for a stream using a valid Token
943          * @param array $params An associative array of optional general parameters.
944          * This will likely change as the API evolves, as of this writing allowed keys
945          * are: count, before_id, since_id, include_muted, include_deleted,
946          * include_directed_posts, and include_annotations.
947          * @return An array of associative arrays, each representing a single post.
948          */
949         public function getTokenStream($params = array()) {
950                 if ($params['access_token']) {
951                         return $this->httpReq('get',$this->_baseUrl.'posts/stream?'.$this->buildQueryString($params),$params);
952                 } else {
953                         return $this->httpReq('get',$this->_baseUrl.'posts/stream?'.$this->buildQueryString($params));
954                 }
955         }
956
957         /**
958          * Get a user object by username
959          * @param string $name the @name to get
960          * @return array representing one user
961          */
962         public function getUserByName($name=null) {
963                 return $this->httpReq('get',$this->_baseUrl.'users/@'.$name);
964         }
965
966         /**
967         * Return the 20 most recent Posts from the current User's personalized stream
968         * and mentions stream merged into one stream.
969         * @param array $params An associative array of optional general parameters.
970         * This will likely change as the API evolves, as of this writing allowed keys
971         * are: count, before_id, since_id, include_muted, include_deleted,
972         * include_directed_posts, and include_annotations.
973         * @return An array of associative arrays, each representing a single post.
974         */
975         public function getUserUnifiedStream($params = array()) {
976                 return $this->httpReq('get',$this->_baseUrl.'posts/stream/unified?'.$this->buildQueryString($params));
977         }
978
979         /**
980          * Update Profile Data via JSON
981          * @data array containing user descriptors
982          */
983         public function updateUserData($data = array(), $params = array()) {
984                 $json = json_encode($data);
985                 return $this->httpReq('put',$this->_baseUrl.'users/me'.'?'.
986                                                 $this->buildQueryString($params), $json, 'application/json');
987         }
988
989         /**
990          * Update a user image
991          * @which avatar|cover
992          * @image path reference to image
993          */
994         protected function updateUserImage($which = 'avatar', $image = null) {
995                 $data = array($which=>"@$image");
996                 return $this->httpReq('post-raw',$this->_baseUrl.'users/me/'.$which, $data, 'multipart/form-data');
997         }
998
999         public function updateUserAvatar($avatar = null) {
1000                 if($avatar != null)
1001                         return $this->updateUserImage('avatar', $avatar);
1002         }
1003
1004         public function updateUserCover($cover = null) {
1005                 if($cover != null)
1006                         return $this->updateUserImage('cover', $cover);
1007         }
1008
1009         /**
1010          * update stream marker
1011          */
1012         public function updateStreamMarker($data = array()) {
1013                 $json = json_encode($data);
1014                 return $this->httpReq('post',$this->_baseUrl.'posts/marker', $json, 'application/json');
1015         }
1016
1017         /**
1018          * get a page of current user subscribed channels
1019          */
1020         public function getUserSubscriptions($params = array()) {
1021                 return $this->httpReq('get',$this->_baseUrl.'channels?'.$this->buildQueryString($params));
1022         }
1023
1024         /**
1025          * get user channels
1026          */
1027         public function getMyChannels($params = array()) {
1028                 return $this->httpReq('get',$this->_baseUrl.'channels/me?'.$this->buildQueryString($params));
1029         }
1030
1031         /**
1032          * create a channel
1033          * note: you cannot create a channel with type=net.app.core.pm (see createMessage)
1034          */
1035         public function createChannel($data = array()) {
1036                 $json = json_encode($data);
1037                 return $this->httpReq('post',$this->_baseUrl.'channels'.($pm?'/pm/messsages':''), $json, 'application/json');
1038         }
1039
1040         /**
1041          * get channelid info
1042          */
1043         public function getChannel($channelid, $params = array()) {
1044                 return $this->httpReq('get',$this->_baseUrl.'channels/'.$channelid.'?'.$this->buildQueryString($params));
1045         }
1046
1047         /**
1048          * get multiple channels' info by an array of channelids
1049          */
1050         public function getChannels($channels, $params = array()) {
1051                 return $this->httpReq('get',$this->_baseUrl.'channels?ids='.join(',',$channels).'&'.$this->buildQueryString($params));
1052         }
1053
1054         /**
1055          * update channelid
1056          */
1057         public function updateChannel($channelid, $data = array()) {
1058                 $json = json_encode($data);
1059                 return $this->httpReq('put',$this->_baseUrl.'channels/'.$channelid, $json, 'application/json');
1060         }
1061
1062         /**
1063          * subscribe from channelid
1064          */
1065         public function channelSubscribe($channelid) {
1066                 return $this->httpReq('post',$this->_baseUrl.'channels/'.$channelid.'/subscribe');
1067         }
1068
1069         /**
1070          * unsubscribe from channelid
1071          */
1072         public function channelUnsubscribe($channelid) {
1073                 return $this->httpReq('delete',$this->_baseUrl.'channels/'.$channelid.'/subscribe');
1074         }
1075
1076         /**
1077          * get all user objects subscribed to channelid
1078          */
1079         public function getChannelSubscriptions($channelid, $params = array()) {
1080                 return $this->httpReq('get',$this->_baseUrl.'channel/'.$channelid.'/subscribers?'.$this->buildQueryString($params));
1081         }
1082
1083         /**
1084          * get all user IDs subscribed to channelid
1085          */
1086         public function getChannelSubscriptionsById($channelid) {
1087                 return $this->httpReq('get',$this->_baseUrl.'channel/'.$channelid.'/subscribers/ids');
1088         }
1089
1090
1091         /**
1092          * get a page of messages in channelid
1093          */
1094         public function getMessages($channelid, $params = array()) {
1095                 return $this->httpReq('get',$this->_baseUrl.'channels/'.$channelid.'/messages?'.$this->buildQueryString($params));
1096         }
1097
1098         /**
1099          * create message
1100          * @param $channelid numeric or "pm" for auto-chanenl (type=net.app.core.pm)
1101          * @param $data array('text'=>'YOUR_MESSAGE') If a type=net.app.core.pm, then "destinations" key can be set to address as an array of people to send this PM too
1102          */
1103         public function createMessage($channelid,$data) {
1104                 $json = json_encode($data);
1105                 return $this->httpReq('post',$this->_baseUrl.'channels/'.$channelid.'/messages', $json, 'application/json');
1106         }
1107
1108         /**
1109          * get message
1110          */
1111         public function getMessage($channelid,$messageid) {
1112                 return $this->httpReq('get',$this->_baseUrl.'channels/'.$channelid.'/messages/'.$messageid);
1113         }
1114
1115         /**
1116          * delete messsage
1117          */
1118         public function deleteMessage($channelid,$messageid) {
1119                 return $this->httpReq('delete',$this->_baseUrl.'channels/'.$channelid.'/messages/'.$messageid);
1120         }
1121
1122
1123         /**
1124          * Get Application Information
1125          */
1126         public function getAppTokenInfo() {
1127                 // requires appAccessToken
1128                 if (!$this->_appAccessToken) {
1129                         $this->getAppAccessToken();
1130                 }
1131                 // ensure request is made with our appAccessToken
1132                 $params['access_token']=$this->_appAccessToken;
1133                 return $this->httpReq('get',$this->_baseUrl.'token',$params);
1134         }
1135
1136         /**
1137          * Get User Information
1138          */
1139         public function getUserTokenInfo() {
1140                 return $this->httpReq('get',$this->_baseUrl.'token');
1141         }
1142
1143         /**
1144          * Get Application Authorized User IDs
1145          */
1146         public function getAppUserIDs() {
1147                 // requires appAccessToken
1148                 if (!$this->_appAccessToken) {
1149                         $this->getAppAccessToken();
1150                 }
1151                 // ensure request is made with our appAccessToken
1152                 $params['access_token']=$this->_appAccessToken;
1153                 return $this->httpReq('get',$this->_baseUrl.'apps/me/tokens/user_ids',$params);
1154         }
1155
1156         /**
1157          * Get Application Authorized User Tokens
1158          */
1159         public function getAppUserTokens() {
1160                 // requires appAccessToken
1161                 if (!$this->_appAccessToken) {
1162                         $this->getAppAccessToken();
1163                 }
1164                 // ensure request is made with our appAccessToken
1165                 $params['access_token']=$this->_appAccessToken;
1166                 return $this->httpReq('get',$this->_baseUrl.'apps/me/tokens',$params);
1167         }
1168
1169         public function getLastRequest() {
1170                 return $this->_last_request;
1171         }
1172         public function getLastResponse() {
1173                 return $this->_last_response;
1174         }
1175
1176         /**
1177          * Registers your function (or an array of object and method) to be called
1178          * whenever an event is received via an open app.net stream. Your function
1179          * will receive a single parameter, which is the object wrapper containing
1180          * the meta and data.
1181          * @param mixed A PHP callback (either a string containing the function name,
1182          * or an array where the first element is the class/object and the second
1183          * is the method).
1184          */
1185         public function registerStreamFunction($function) {
1186                 $this->_streamCallback = $function;
1187         }
1188
1189         /**
1190          * Opens a stream that's been created for this user/app and starts sending
1191          * events/objects to your defined callback functions. You must define at
1192          * least one callback function before opening a stream.
1193          * @param mixed $stream Either a stream ID or the endpoint of a stream
1194          * you've already created. This stream must exist and must be valid for
1195          * your current access token. If you pass a stream ID, the library will
1196          * make an API call to get the endpoint.
1197          *
1198          * This function will return immediately, but your callback functions
1199          * will continue to receive events until you call closeStream() or until
1200          * App.net terminates the stream from their end with an error.
1201          *
1202          * If you're disconnected due to a network error, the library will
1203          * automatically attempt to reconnect you to the same stream, no action
1204          * on your part is necessary for this. However if the app.net API returns
1205          * an error, a reconnection attempt will not be made.
1206          *
1207          * Note there is no closeStream, because once you open a stream you
1208          * can't stop it (unless you exit() or die() or throw an uncaught
1209          * exception, or something else that terminates the script).
1210          * @return boolean True
1211          * @see createStream()
1212          */
1213         public function openStream($stream) {
1214                 // if there's already a stream running, don't allow another
1215                 if ($this->_currentStream) {
1216                         throw new AppDotNetException('There is already a stream being consumed, only one stream can be consumed per AppDotNetStream instance');
1217                 }
1218                 // must register a callback (or the exercise is pointless)
1219                 if (!$this->_streamCallback) {
1220                         throw new AppDotNetException('You must define your callback function using registerStreamFunction() before calling openStream');
1221                 }
1222                 // if the stream is a numeric value, get the stream info from the api
1223                 if (is_numeric($stream)) {
1224                         $stream = $this->getStream($stream);
1225                         $this->_streamUrl = $stream['endpoint'];
1226                 }
1227                 else {
1228                         $this->_streamUrl = $stream;
1229                 }
1230                 // continue doing this until we get an error back or something...?
1231                 $this->httpStream('get',$this->_streamUrl);
1232
1233                 return true;
1234         }
1235
1236         /**
1237          * Close the currently open stream.
1238          * @return true;
1239          */
1240         public function closeStream() {
1241                 if (!$this->_lastStreamActivity) {
1242                         // never opened
1243                         return;
1244                 }
1245                 if (!$this->_multiStream) {
1246                         throw new AppDotNetException('You must open a stream before calling closeStream()');
1247                 }
1248                 curl_close($this->_currentStream);
1249                 curl_multi_remove_handle($this->_multiStream,$this->_currentStream);
1250                 curl_multi_close($this->_multiStream);
1251                 $this->_currentStream = null;
1252                 $this->_multiStream = null;
1253         }
1254
1255         /**
1256          * Retrieve all streams for the current access token.
1257          * @return array An array of stream definitions.
1258          */
1259         public function getAllStreams() {
1260                 return $this->httpReq('get',$this->_baseUrl.'streams');
1261         }
1262
1263         /**
1264          * Returns a single stream specified by a stream ID. The stream must have been
1265          * created with the current access token.
1266          * @return array A stream definition
1267          */
1268         public function getStream($streamId) {
1269                 return $this->httpReq('get',$this->_baseUrl.'streams/'.urlencode($streamId));
1270         }
1271
1272         /**
1273          * Creates a stream for the current app access token.
1274          *
1275          * @param array $objectTypes The objects you want to retrieve data for from the
1276          * stream. At time of writing these can be 'post', 'star', and/or 'user_follow'.
1277          * If you don't specify, all events will be retrieved.
1278          */
1279         public function createStream($objectTypes=null) {
1280                 // default object types to everything
1281                 if (is_null($objectTypes)) {
1282                         $objectTypes = array('post','star','user_follow');
1283                 }
1284                 $data = array(
1285                         'object_types'=>$objectTypes,
1286                         'type'=>'long_poll',
1287                 );
1288                 $data = json_encode($data);
1289                 $response = $this->httpReq('post',$this->_baseUrl.'streams',$data,'application/json');
1290                 return $response;
1291         }
1292
1293         /**
1294          * Update stream for the current app access token
1295          *
1296          * @param integer $streamId The stream ID to update. This stream must have been
1297          * created by the current access token.
1298          * @param array $data allows object_types, type, filter_id and key to be updated. filter_id/key can be omitted
1299          */
1300         public function updateStream($streamId,$data) {
1301                 // objectTypes is likely required
1302                 if (is_null($data['object_types'])) {
1303                         $data['object_types'] = array('post','star','user_follow');
1304                 }
1305                 // type can still only be long_poll
1306                 if (is_null($data['type'])) {
1307                          $data['type']='long_poll';
1308                 }
1309                 $data = json_encode($data);
1310                 $response = $this->httpReq('put',$this->_baseUrl.'streams/'.urlencode($streamId),$data,'application/json');
1311                 return $response;
1312          }
1313
1314         /**
1315          * Deletes a stream if you no longer need it.
1316          *
1317          * @param integer $streamId The stream ID to delete. This stream must have been
1318          * created by the current access token.
1319          */
1320         public function deleteStream($streamId) {
1321                 return $this->httpReq('delete',$this->_baseUrl.'streams/'.urlencode($streamId));
1322         }
1323
1324         /**
1325          * Deletes all streams created by the current access token.
1326          */
1327         public function deleteAllStreams() {
1328                 return $this->httpReq('delete',$this->_baseUrl.'streams');
1329         }
1330
1331         /**
1332          * Internal function used to process incoming chunks from the stream. This is only
1333          * public because it needs to be accessed by CURL. Do not call or use this function
1334          * in your own code.
1335          * @ignore
1336          */
1337         public function httpStreamReceive($ch,$data) {
1338                 $this->_lastStreamActivity = time();
1339                 $this->_streamBuffer .= $data;
1340                 if (!$this->_streamHeaders) {
1341                         $pos = strpos($this->_streamBuffer,"\r\n\r\n");
1342                         if ($pos!==false) {
1343                                 $this->_streamHeaders = substr($this->_streamBuffer,0,$pos);
1344                                 $this->_streamBuffer = substr($this->_streamBuffer,$pos+4);
1345                         }
1346                 }
1347                 else {
1348                         $pos = strpos($this->_streamBuffer,"\r\n");
1349                         while ($pos!==false) {
1350                                 $command = substr($this->_streamBuffer,0,$pos);
1351                                 $this->_streamBuffer = substr($this->_streamBuffer,$pos+2);
1352                                 $command = json_decode($command,true);
1353                                 if ($command) {
1354                                         call_user_func($this->_streamCallback,$command);
1355                                 }
1356                                 $pos = strpos($this->_streamBuffer,"\r\n");
1357                         }
1358                 }
1359                 return strlen($data);
1360         }
1361
1362         /**
1363          * Opens a long lived HTTP connection to the app.net servers, and sends data
1364          * received to the httpStreamReceive function. As a general rule you should not
1365          * directly call this method, it's used by openStream().
1366          */
1367         protected function httpStream($act, $req, $params=array(),$contentType='application/x-www-form-urlencoded') {
1368                 if ($this->_currentStream) {
1369                         throw new AppDotNetException('There is already an open stream, you must close the existing one before opening a new one');
1370                 }
1371                 $headers = array();
1372                 $this->_streamBuffer = '';
1373                 if ($this->_accessToken) {
1374                         $headers[] = 'Authorization: Bearer '.$this->_accessToken;
1375                 }
1376                 $this->_currentStream = curl_init($req);
1377                 curl_setopt($this->_currentStream, CURLOPT_HTTPHEADER, $headers);
1378                 curl_setopt($this->_currentStream, CURLOPT_RETURNTRANSFER, true);
1379                 curl_setopt($this->_currentStream, CURLINFO_HEADER_OUT, true);
1380                 curl_setopt($this->_currentStream, CURLOPT_HEADER, true);
1381                 if ($this->_sslCA) {
1382                         curl_setopt($this->_currentStream, CURLOPT_CAINFO, $this->_sslCA);
1383                 }
1384                 // every time we receive a chunk of data, forward it to httpStreamReceive
1385                 curl_setopt($this->_currentStream, CURLOPT_WRITEFUNCTION, array($this, "httpStreamReceive"));
1386
1387                 // curl_exec($ch);
1388                 // return;
1389
1390                 $this->_multiStream = curl_multi_init();
1391                 $this->_lastStreamActivity = time();
1392                 curl_multi_add_handle($this->_multiStream,$this->_currentStream);
1393         }
1394
1395         public function reconnectStream() {
1396                 $this->closeStream();
1397                 $this->_connectFailCounter++;
1398                 // if we've failed a few times, back off
1399                 if ($this->_connectFailCounter>1) {
1400                         $sleepTime = pow(2,$this->_connectFailCounter);
1401                         // don't sleep more than 60 seconds
1402                         if ($sleepTime>60) {
1403                                 $sleepTime = 60;
1404                         }
1405                         sleep($sleepTime);
1406                 }
1407                 $this->httpStream('get',$this->_streamUrl);
1408         }
1409
1410         /**
1411          * Process an open stream for x microseconds, then return. This is useful if you want
1412          * to be doing other things while processing the stream. If you just want to
1413          * consume the stream without other actions, you can call processForever() instead.
1414          * @param float @microseconds The number of microseconds to process for before
1415          * returning. There are 1,000,000 microseconds in a second.
1416          *
1417          * @return void
1418          */
1419         public function processStream($microseconds=null) {
1420                 if (!$this->_multiStream) {
1421                         throw new AppDotNetException('You must open a stream before calling processStream()');
1422                 }
1423                 $start = microtime(true);
1424                 $active = null;
1425                 $inQueue = null;
1426                 $sleepFor = 0;
1427                 do {
1428                         // if we haven't received anything within 5.5 minutes, reconnect
1429                         // keepalives are sent every 5 minutes (measured on 2013-3-12 by @ryantharp)
1430                         if (time()-$this->_lastStreamActivity>=330) {
1431                                 $this->reconnectStream();
1432                         }
1433                         curl_multi_exec($this->_multiStream, $active);
1434                         if (!$active) {
1435                                 $httpCode = curl_getinfo($this->_currentStream,CURLINFO_HTTP_CODE);
1436                                 // don't reconnect on 400 errors
1437                                 if ($httpCode>=400 && $httpCode<=499) {
1438                                         throw new AppDotNetException('Received HTTP error '.$httpCode.' check your URL and credentials before reconnecting');
1439                                 }
1440                                 $this->reconnectStream();
1441                         }
1442                         // sleep for a max of 2/10 of a second
1443                         $timeSoFar = (microtime(true)-$start)*1000000;
1444                         $sleepFor = $this->streamingSleepFor;
1445                         if ($timeSoFar+$sleepFor>$microseconds) {
1446                                 $sleepFor = $microseconds - $timeSoFar;
1447                         }
1448
1449                         if ($sleepFor>0) {
1450                                 usleep($sleepFor);
1451                         }
1452                 } while ($timeSoFar+$sleepFor<$microseconds);
1453         }
1454
1455         /**
1456          * Process an open stream forever. This function will never return, if you
1457          * want to perform other actions while consuming the stream, you should use
1458          * processFor() instead.
1459          * @return void This function will never return
1460          * @see processFor();
1461          */
1462         public function processStreamForever() {
1463                 while (true) {
1464                         $this->processStream(600);
1465                 }
1466         }
1467
1468
1469         /**
1470          * Upload a file to a user's file store
1471          * @param string $file A string containing the path of the file to upload.
1472          * @param array $data Additional data about the file you're uploading. At the
1473          * moment accepted keys are: mime-type, kind, type, name, public and annotations.
1474          * - If you don't specify mime-type, ADNPHP will attempt to guess the mime type
1475          * based on the file, however this isn't always reliable.
1476          * - If you don't specify kind ADNPHP will attempt to determine if the file is
1477          * an image or not.
1478          * - If you don't specify name, ADNPHP will use the filename of the first
1479          * parameter.
1480          * - If you don't specify public, your file will be uploaded as a private file.
1481          * - Type is REQUIRED.
1482          * @param array $params An associative array of optional general parameters.
1483          * This will likely change as the API evolves, as of this writing allowed keys
1484          * are: include_annotations|include_file_annotations.
1485          * @return array An associative array representing the file
1486          */
1487         public function createFile($file, $data, $params=array()) {
1488                 if (!$file) {
1489                         throw new AppDotNetException('You must specify a path to a file');
1490                 }
1491                 if (!file_exists($file)) {
1492                         throw new AppDotNetException('File path specified does not exist');
1493                 }
1494                 if (!is_readable($file)) {
1495                         throw new AppDotNetException('File path specified is not readable');
1496                 }
1497
1498                 if (!$data) {
1499                         $data = array();
1500                 }
1501
1502                 if (!array_key_exists('type',$data) || !$data['type']) {
1503                         throw new AppDotNetException('Type is required when creating a file');
1504                 }
1505
1506                 if (!array_key_exists('name',$data)) {
1507                         $data['name'] = basename($file);
1508                 }
1509
1510                 if (array_key_exists('mime-type',$data)) {
1511                         $mimeType = $data['mime-type'];
1512                         unset($data['mime-type']);
1513                 }
1514                 else {
1515                         $mimeType = null;
1516                 }
1517                 if (!array_key_exists('kind',$data)) {
1518                         $test = @getimagesize($path);
1519                         if ($test && array_key_exists('mime',$test)) {
1520                                 $data['kind'] = 'image';
1521                                 if (!$mimeType) {
1522                                         $mimeType = $test['mime'];
1523                                 }
1524                         }
1525                         else {
1526                                 $data['kind'] = 'other';
1527                         }
1528                 }
1529                 if (!$mimeType) {
1530                         $finfo = finfo_open(FILEINFO_MIME_TYPE);
1531                         $mimeType = finfo_file($finfo, $file);
1532                         finfo_close($finfo);
1533                 }
1534                 if (!$mimeType) {
1535                         throw new AppDotNetException('Unable to determine mime type of file, try specifying it explicitly');
1536                 }
1537                 if (!array_key_exists('public',$data) || !$data['public']) {
1538                         $public = false;
1539                 }
1540                 else {
1541                         $public = true;
1542                 }
1543
1544                 $data['content'] = "@$file;type=$mimeType";
1545                 return $this->httpReq('post-raw',$this->_baseUrl.'files', $data, 'multipart/form-data');
1546         }
1547
1548
1549         public function createFilePlaceholder($file = null, $params=array()) {
1550                 $name = basename($file);
1551                 $data = array('annotations' => $params['annotations'], 'kind' => $params['kind'],
1552                                 'name' => $name, 'type' => $params['metadata']);
1553                 $json = json_encode($data);
1554                 return $this->httpReq('post',$this->_baseUrl.'files', $json, 'application/json');
1555         }
1556
1557         public function updateFileContent($fileid, $file) {
1558
1559                 $data = file_get_contents($file);
1560                 $finfo = finfo_open(FILEINFO_MIME_TYPE);
1561                 $mime = finfo_file($finfo, $file);
1562                 finfo_close($finfo);
1563
1564                 return $this->httpReq('put',$this->_baseUrl.'files/' . $fileid
1565                                                 .'/content', $data, $mime);
1566         }
1567
1568                 /**
1569                  * Allows for file rename and annotation changes.
1570                  * @param integer $file_id The ID of the file to update
1571                  * @param array $params An associative array of file parameters.
1572                  * @return array An associative array representing the updated file
1573                 */
1574                 public function updateFile($file_id=null, $params=array()) {
1575                         $data = array('annotations' => $params['annotations'] , 'name' => $params['name']);
1576                         $json = json_encode($data);
1577                         return $this->httpReq('put',$this->_baseUrl.'files/'.urlencode($file_id), $json, 'application/json');
1578                 }
1579
1580         /**
1581          * Returns a specific File.
1582          * @param integer $file_id The ID of the file to retrieve
1583          * @param array $params An associative array of optional general parameters.
1584          * This will likely change as the API evolves, as of this writing allowed keys
1585          * are: include_annotations|include_file_annotations.
1586          * @return array An associative array representing the file
1587          */
1588         public function getFile($file_id=null,$params = array()) {
1589                 return $this->httpReq('get',$this->_baseUrl.'files/'.urlencode($file_id)
1590                                         .'?'.$this->buildQueryString($params));
1591         }
1592
1593         public function getFileContent($file_id=null,$params = array()) {
1594                 return $this->httpReq('get',$this->_baseUrl.'files/'.urlencode($file_id)
1595                                         .'/content?'.$this->buildQueryString($params));
1596         }
1597
1598         /** $file_key : derived_file_key */
1599         public function getDerivedFileContent($file_id=null,$file_key=null,$params = array()) {
1600                 return $this->httpReq('get',$this->_baseUrl.'files/'.urlencode($file_id)
1601                                         .'/content/'.urlencode($file_key)
1602                                         .'?'.$this->buildQueryString($params));
1603         }
1604
1605         /**
1606          * Returns file objects.
1607          * @param array $file_ids The IDs of the files to retrieve
1608          * @param array $params An associative array of optional general parameters.
1609          * This will likely change as the API evolves, as of this writing allowed keys
1610          * are: include_annotations|include_file_annotations.
1611          * @return array An associative array representing the file data.
1612          */
1613         public function getFiles($file_ids=array(), $params = array()) {
1614                 $ids = '';
1615                 foreach($file_ids as $id) {
1616                         $ids .= $id . ',';
1617                 }
1618                 $params['ids'] = substr($ids, 0, -1);
1619                 return $this->httpReq('get',$this->_baseUrl.'files'
1620                                         .'?'.$this->buildQueryString($params));
1621         }
1622
1623         /**
1624          * Returns a user's file objects.
1625          * @param array $params An associative array of optional general parameters.
1626          * This will likely change as the API evolves, as of this writing allowed keys
1627          * are: include_annotations|include_file_annotations|include_user_annotations.
1628          * @return array An associative array representing the file data.
1629          */
1630         public function getUserFiles($params = array()) {
1631                 return $this->httpReq('get',$this->_baseUrl.'users/me/files'
1632                                                 .'?'.$this->buildQueryString($params));
1633         }
1634
1635         /**
1636          * Delete a File. The current user must be the same user who created the File.
1637          * It returns the deleted File on success.
1638          * @param integer $file_id The ID of the file to delete
1639          * @return array An associative array representing the file that was deleted
1640          */
1641         public function deleteFile($file_id=null) {
1642                 return $this->httpReq('delete',$this->_baseUrl.'files/'.urlencode($file_id));
1643         }
1644
1645 }
1646
1647 class AppDotNetException extends Exception {}