<?php

/**
 * @file
 * Provides the Disqus PHP API.
 */

/**
 * The Disqus PHP API.
 *
 * @package    Disqus
 * @author     Rob Loach (http://www.robloach.net)
 * @version    1.1.0
 * @access     public
 * @license    GPL (http://www.opensource.org/licenses/gpl-2.0.php)
 *
 * @example
 * @code
 *   // The user API key obtained from http://disqus.com/api/get_my_key/ .
 *   $user_api_key = 'Your Key Here!';
 *
 *   // Make sure to catch any errors generated by Disqus.
 *   try {
 *     $disqus = new Disqus($user_api_key);
 *
 *     // Get the forum list.
 *     $forums = $disqus->get_forum_list();
 *
 *     foreach ($forums as $forum) {
 *       echo $forum->name;
 *     }
 *   } catch(DisqusException $exception) {
 *     // Display the error.
 *     echo $exception->getMessage();
 *   }
 * @endcode
 */
class Disqus {
  public $user_api_key = NULL;
  public $forum_api_key = NULL;
  public $api_url = 'http://disqus.com/api/';
  public $api_version = '1.1';

  /**
   * Creates a new interface to the Disqus API.
   *
   * @param $user_api_key
   *   (optional) The User API key to use.
   * @param $forum_api_key
   *   (optional) The Forum API key to use.
   * @param $api_url
   *   (optional) The prefix URL to use when calling the Disqus API.
   */
  function __construct($user_api_key = NULL, $forum_api_key = NULL, $api_url = 'http://disqus.com/api/') {
    $this->user_api_key = $user_api_key;
    $this->forum_api_key = $forum_api_key;
    $this->$api_url = $api_url;
  }

  /**
   * Validate API key and get username.
   */
  function get_user_name() {
    return $this->call('get_user_name', array(), TRUE);
  }

  /**
   * Get a forum API key for a specific forum.
   *
   * @param $forum_id
   *   the unique id of the forum
   * @return
   *   A string which is the Forum Key for the given forum.
   */
  function get_forum_api_key($forum_id) {
    return $this->call('get_forum_api_key', array('forum_id' => $forum_id));
  }

  /**
   * Returns an array of hashes representing all forums the user owns.
   *
   * @return
   *   An array of hashes representing all forums the user owns.
   */
  function get_forum_list() {
    return $this->call('get_forum_list');
  }

  /**
   * Get a list of comments on a website.
   *
   * Both filter and exclude are multivalue arguments with coma as a divider.
   * That makes is possible to use combined requests. For example, if you want
   * to get all deleted spam messages, your filter argument should contain
   * 'spam,killed' string.
   *
   * @param $forum_id
   *   The forum ID.
   * @param $options
   *   - limit: Number of entries that should be included in the response. Default is 25.
   *   - start: Starting point for the query. Default is 0.
   *   - filter: Type of entries that should be returned.
   *   - exclude: Type of entries that should be excluded from the response.
   * @return
   *   Returns posts from a forum specified by id.
   */
  function get_forum_posts($forum_id, array $options = array()) {
    $options['forum_id'] = $forum_id;
    return $this->call('get_forum_posts', $options);
  }

  /**
   * Count a number of comments in articles.
   *
   * @param $thread_ids
   *   an array of thread IDs belonging to the given forum.
   * @return
   *   A hash having thread_ids as keys and 2-element arrays as values.
   */
  function get_num_posts(array $thread_ids = array()) {
    $thread_ids = implode(',', $thread_ids);
    return $this->call('get_num_posts', array('thread_ids' => $thread_ids));
  }

  /**
   * Get a list of threads on a website.
   *
   * @param $forum_id
   *   the unique id of the forum.
   * @param $limit
   *   Number of entries that should be included in the response.
   * @param $start
   *   Starting point for the query.
   * @return
   *   An array of hashes representing all threads belonging to the given forum.
   */
  function get_thread_list($forum_id, $limit = 25, $start = 0) {
    return $this->call('get_thread_list', array('forum_id' => $forum_id, 'limit' => $limit, 'start' => 0));
  }

  /**
   * Get a list of threads with new comments.
   *
   * @param $forum_id
   *   The Forum ID.
   * @param $since
   *   Start date for new posts. Format: 2009-03-30T15:41, Timezone: UTC.
   */
  function get_updated_threads($forum_id, $since) {
    return $this->call('get_updated_threads', array('forum_id' => $forum_id, 'since' => $since));
  }

  /**
   * Get a list of comments in a thread.
   *
   * Both filter and exclude are multivalue arguments with coma as a divider.
   * That makes is possible to use combined requests. For example, if you want
   * to get all deleted spam messages, your filter argument should contain
   * 'spam,killed' string. Note that values are joined by AND statement so
   * 'spam,new' will return all messages that are new and marked as spam. It
   * will not return messages that are new and not spam or that are spam but
   * not new (i.e. has already been moderated).
   *
   * @param $thread_id
   *   The ID of a thread belonging to the given forum
   * @param $limit
   *   - limit: Number of entries that should be included in the response. Default is 25.
   *   - start: Starting point for the query. Default is 0.
   *   - filter: Type of entries that should be returned (new, spam or killed).
   *   - exclude: Type of entries that should be excluded from the response (new, spam or killed).
   * @return
   *   An array of hashes representing representing all posts belonging to the
   *   given forum.
   */
  function get_thread_posts($thread_id, array $options = array()) {
    $options['thread_id'] = $thread_id;
    return $this->call('get_thread_posts', $options);
  }

  /**
   * Get or create thread by identifier.
   *
   * This method tries to find a thread by its identifier and title. If there is
   * no such thread, the method creates it. In either case, the output value is
   * a thread object.
   *
   * @param $identifier
   *   Unique value (per forum) for a thread that is used to keep be able to get
   *   data even if permalink is changed.
   * @param $title
   *   The title of the thread to possibly be created.
   * @return
   *   Returns a hash with two keys:
   *   - thread: a hash representing the thread corresponding to the identifier.
   *   - created: indicates whether the thread was created as a result of this
   *     method call. If created, it will have the specified title.
   */
  function thread_by_identifier($identifier, $title) {
    return $this->call('thread_by_identifier', array('title' => $title, 'identifier' => $identifier), TRUE);
  }

  /**
   * Get thread by URL.
   *
   * Finds a thread by its URL. Output value is a thread object.
   *
   * @param $url
   *   the URL to check for an associated thread
   * @param $partner_api_key
   *   (optional) The Partner API key.
   * @return
   *   A thread object, otherwise NULL.
   */
  function get_thread_by_url($url, $partner_api_key = NULL) {
    return $this->call('get_thread_by_url', array('url' => $url));
  }

  /**
   * Updates thread.
   *
   * Updates thread, specified by id and forum API key, with values described in
   * the optional arguments.
   *
   * @param $thread_id
   *   the ID of a thread belonging to the given forum
   * @param $options
   *   - title: the title of the thread
   *   - slug: the per-forum-unique string used for identifying this thread in
   *           disqus.com URL’s relating to this thread. Composed of
   *           underscore-separated alphanumeric strings.
   *   - url: the URL this thread is on, if known.
   *   - allow_comments: whether this thread is open to new comments
   * @return
   *   Returns an empty success message.
   */
  function update_thread($thread_id, array $options = array()) {
    $options['thread_id'] = $thread_id;
    return $this->call('update_thread', $options, TRUE);
  }

  /**
   * Creates a new post.
   *
   * Creates a comment to the thread specified by id.
   *
   * @param $thread_id
   *   the thread to post to
   * @param $message
   *   the content of the post
   * @param $author_name
   *   the post creator’s name
   * @param $author_email
   *   the post creator’s email address
   * @param $options
   *   - partner_api_key
   *   - created_at: Format: 2009-03-30T15:41, Timezone: UTC
   *   - ip_address: the author’s IP address
   *   - author_url: the author's homepage
   *   - parent_post: the id of the parent post
   *   - state: Comment's state, must be one of the following: approved,
   *            unapproved, spam, killed
   */
  function create_post($thread_id, $message, $author_name, $author_email, array $options = array()) {
    $options['thread_id'] = $thread_id;
    $options['message'] = $message;
    $options['author_name'] = $author_name;
    $options['author_email'] = $author_email;
    return $this->call('create_post', $options, TRUE);
  }

  /**
   * Delete a comment or mark it as spam (or not spam).
   *
   * @param $post_id
   *   The Post ID.
   * @param $action
   *   Name of action to be performed. Value can be 'spam', 'approve' or 'kill'.
   *
   * @return
   *   Returns modified version.
   */
  function moderate_post($post_id, $action) {
    return $this->call('create_post', array('post_id' => $post_id, 'action' => $action), TRUE);
  }

  /**
   * Makes a call to a Disqus API method.
   *
   * @return 
   *   The Disqus object.
   * @param $method
   *   The Disqus API method to call.
   * @param object $arguments
   *   An associative array of arguments to be passed.
   * @param $post
   *   TRUE or FALSE, depending on whether we're making a POST call.
   */
  function call($function, $arguments = array(), $post = FALSE) {
    // Construct the arguments.
    $args = '';
    if (!isset($arguments['user_api_key'])) {
      $arguments['user_api_key'] = $this->user_api_key;
    }
    if (!isset($arguments['forum_api_key'])) {
      $arguments['forum_api_key'] = $this->forum_api_key;
    }
    if (!isset($arguments['api_version'])) {
      $arguments['api_version'] = $this->api_version;
    }
    foreach ($arguments as $argument => $value) {
      if (!empty($value)) {
        $args .= $argument .'='. urlencode($value) .'&';
      }
    }

    // Call the Disqus API.
    $ch = curl_init();
    if ($post) {
      curl_setopt($ch, CURLOPT_URL, $this->api_url . $function .'/');
      curl_setopt($ch, CURLOPT_POSTFIELDS, $args);
      curl_setopt($ch, CURLOPT_POST, 1);
    }
    else {
      curl_setopt($ch, CURLOPT_URL, $this->api_url . $function .'/?'. $args);
    }
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    $data = curl_exec($ch);
    $info = curl_getinfo($ch);
    curl_close($ch);
    
    // Check the results for errors (200 is a successful HTTP call).
    if ($info['http_code'] == 200) {
      $disqus = json_decode($data);
      if ($disqus->succeeded) {
        // There weren't any errors, so return the results.
        return isset($disqus->message) ? $disqus->message : NULL;
      }
      else {
        throw new DisqusException(isset($disqus->message) ? $disqus->message : NULL, 0, $info, $disqus);
      }
    }
    else {
      throw new DisqusException('There was an error querying the Disqus API.', $info['http_code'], $info);
    }
  }
}

/**
 * Any unsucessful result that's created by Disqus API will generate a DisqusException.
 */
class DisqusException extends Exception {
  /**
   * The information returned from the cURL call.
   */
  public $info = NULL;

  /**
   * The information returned from the Disqus call.
   */
  public $disqus = NULL;

  /**
   * Creates a DisqusException.
   * @param $message
   *   The message for the exception.
   * @param $code
   *   (optional) The error code.
   * @param $info
   *   (optional) The result from the cURL call.
   */
  public function __construct($message, $code = 0, $info = NULL, $disqus = NULL) {
    $this->info = $info;
    $this->disqus = $disqus;
    parent::__construct($message, $code);
  }

  /**
   * Converts the exception to a string.
   */
  public function __toString() {
    $code = isset($this->disqus->code) ? $this->disqus->code : (isset($info['http_code']) ? $info['http_code'] : 0);
    $message = $this->getMessage();
    return __CLASS__ .": [$code]: $message\n";
  }
}
