Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First commit: import project. Largely inspired by the Twitter gem fro…

…m John Nunemaker, Wynn Netherland, Erik Michaels-Ober and Steve Richert.
  • Loading branch information...
commit 7cbc6083ad59333ef47b3df7b5086e2d30fd16f5 0 parents
@jeremyvdw authored
Showing with 3,213 additions and 0 deletions.
  1. +3 −0  Gemfile
  2. +82 −0 Gemfile.lock
  3. +2 −0  HISTORY.mkd
  4. +20 −0 LICENSE.mkd
  5. +147 −0 README.mkd
  6. +23 −0 Rakefile
  7. +38 −0 disqussion.gemspec
  8. +25 −0 lib/disqussion.rb
  9. +21 −0 lib/disqussion/api.rb
  10. +46 −0 lib/disqussion/client.rb
  11. BIN  lib/disqussion/client/.DS_Store
  12. +19 −0 lib/disqussion/client/applications.rb
  13. +4 −0 lib/disqussion/client/blacklists.rb
  14. +4 −0 lib/disqussion/client/categories.rb
  15. +4 −0 lib/disqussion/client/exports.rb
  16. +110 −0 lib/disqussion/client/forums.rb
  17. +4 −0 lib/disqussion/client/imports.rb
  18. +4 −0 lib/disqussion/client/posts.rb
  19. +4 −0 lib/disqussion/client/reactions.rb
  20. +4 −0 lib/disqussion/client/reports.rb
  21. +4 −0 lib/disqussion/client/threads.rb
  22. +5 −0 lib/disqussion/client/trends.rb
  23. +103 −0 lib/disqussion/client/users.rb
  24. +90 −0 lib/disqussion/client/utils.rb
  25. +4 −0 lib/disqussion/client/whitelists.rb
  26. +121 −0 lib/disqussion/configuration.rb
  27. +36 −0 lib/disqussion/connection.rb
  28. +51 −0 lib/disqussion/error.rb
  29. +38 −0 lib/disqussion/request.rb
  30. +3 −0  lib/disqussion/version.rb
  31. +9 −0 lib/disqussion/view_helpers.rb
  32. +183 −0 lib/disqussion/widget.rb
  33. +41 −0 lib/faraday/response/raise_http_4xx.rb
  34. +20 −0 lib/faraday/response/raise_http_5xx.rb
  35. +63 −0 spec/disqussion/api_spec.rb
  36. BIN  spec/disqussion/client/.DS_Store
  37. +24 −0 spec/disqussion/client/applications_spec.rb
  38. +21 −0 spec/disqussion/client/forums_spec.rb
  39. +33 −0 spec/disqussion/client/users_spec.rb
  40. +15 −0 spec/disqussion/client_spec.rb
  41. +101 −0 spec/disqussion_spec.rb
  42. +30 −0 spec/faraday/response_spec.rb
  43. +129 −0 spec/fixtures/applications/listUsage.json
  44. +12 −0 spec/fixtures/forums/details.json
  45. +21 −0 spec/fixtures/forums/listCategories.json
  46. +533 −0 spec/fixtures/forums/listThreads.json
  47. +891 −0 spec/fixtures/posts/list.json
  48. +19 −0 spec/fixtures/users/details.json
  49. +4 −0 spec/fixtures/users/follow.json
  50. +45 −0 spec/helper.rb
3  Gemfile
@@ -0,0 +1,3 @@
+source :gemcutter
+
+gemspec
82 Gemfile.lock
@@ -0,0 +1,82 @@
+PATH
+ remote: .
+ specs:
+ disqussion (0.0.1)
+ faraday (~> 0.6.1)
+ faraday_middleware (~> 0.6.3)
+ hashie (~> 1.0.0)
+ multi_json (~> 1.0.0)
+ rash (~> 0.3.0)
+ simple_oauth (~> 0.1.4)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ ZenTest (4.5.0)
+ addressable (2.2.5)
+ archive-tar-minitar (0.5.2)
+ columnize (0.3.2)
+ crack (0.1.8)
+ diff-lcs (1.1.2)
+ faraday (0.6.1)
+ addressable (~> 2.2.4)
+ multipart-post (~> 1.1.0)
+ rack (< 2, >= 1.1.0)
+ faraday_middleware (0.6.3)
+ faraday (~> 0.6.0)
+ hashie (1.0.0)
+ json (1.5.1)
+ linecache19 (0.5.12)
+ ruby_core_source (>= 0.1.4)
+ maruku (0.6.0)
+ syntax (>= 1.0.0)
+ multi_json (1.0.1)
+ multipart-post (1.1.0)
+ nokogiri (1.4.4)
+ rack (1.2.2)
+ rake (0.8.7)
+ rash (0.3.0)
+ hashie (~> 1.0.0)
+ rspec (2.5.0)
+ rspec-core (~> 2.5.0)
+ rspec-expectations (~> 2.5.0)
+ rspec-mocks (~> 2.5.0)
+ rspec-core (2.5.2)
+ rspec-expectations (2.5.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.5.0)
+ ruby-debug-base19 (0.11.25)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby_core_source (>= 0.1.4)
+ ruby-debug19 (0.11.6)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby-debug-base19 (>= 0.11.19)
+ ruby_core_source (0.1.5)
+ archive-tar-minitar (>= 0.5.2)
+ simple_oauth (0.1.4)
+ simplecov (0.4.2)
+ simplecov-html (~> 0.4.4)
+ simplecov-html (0.4.4)
+ syntax (1.0.0)
+ webmock (1.6.2)
+ addressable (>= 2.2.2)
+ crack (>= 0.1.7)
+ yard (0.6.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ ZenTest (~> 4.5)
+ disqussion!
+ json (~> 1.5)
+ maruku (~> 0.6)
+ nokogiri (~> 1.4)
+ rake (~> 0.8)
+ rspec (~> 2.5)
+ ruby-debug19
+ simplecov (~> 0.4)
+ webmock (~> 1.6)
+ yard (~> 0.6)
2  HISTORY.mkd
@@ -0,0 +1,2 @@
+HISTORY
+=======
20 LICENSE.mkd
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Jérémy Van de Wyngaert
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
147 README.mkd
@@ -0,0 +1,147 @@
+The Disqussion Ruby Gem
+====================
+A Ruby wrapper for the Disqus API
+
+Installation
+------------
+``` sh
+gem install disqussion
+```
+
+Documentation
+-------------
+<http://rdoc.info/gems/twitter>
+
+Does your project or organization use this gem?
+-----------------------------------------------
+Add it to the [apps](http://github.com/jeremyvdw/disqussion/wiki/apps) wiki!
+
+Continuous Integration
+----------------------
+[![Build Status](http://travis-ci.org/jnunemaker/twitter.png)](http://travis-ci.org/jnunemaker/twitter)
+
+What's in 0.1?
+------------------
+
+
+```
+The error classes have gone through a transformation to make them consistent with [Disqus's documented response codes](http://disqus.com/api/docs/errors/). These changes should make it easier to rescue from specific errors and take action accordingly. We've also added support for two new classes of error.
+
+<table>
+ <thead>
+ <tr>
+ <th>Response Code</th>
+ <th>Error</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><tt>400</tt></td>
+ <td><tt>Disqus::</tt></td>
+ </tr>
+ <tr>
+ <td><tt>401</tt></td>
+ <td><tt>Disqus::</tt></td>
+ </tr>
+ <tr>
+ <td><tt>403</tt></td>
+ <td><tt>Disqus::</tt></td>
+ </tr>
+ <tr>
+ <td><tt>404</tt></td>
+ <td><tt>Disqus::NotFound</tt></td>
+ </tr>
+ <tr>
+ <td><tt>500</tt></td>
+ <td><tt>Disqus::InternalServerError</tt></td>
+ </tr>
+ </tbody>
+</table>
+
+Here are a few reasons use (and improve) this gem:
+
+* Full Ruby 1.9 compatibility: All code and specs now work in the latest version of Ruby
+* Support for HTTP proxies: Access Disqus from China, Iran, or inside your office firewall
+* Support for multiple HTTP adapters: NetHttp (default), em-net-http, Typhoeus, Patron, or ActionDispatch
+* SSL: On by default for increased [speed](http://gist.github.com/652330) and security
+* Improved error handling: More easily rescue from rate-limit errors or fail whales
+
+Help! I'm getting: "Did not recognize your engine specification. Please specify either a symbol or a class. (RuntimeError)"
+---------------------------------------------------------------------------------------------------------------------------
+
+If you're using the JSON request format (i.e., the default), you'll need to
+explicitly require a JSON library. We recommend [yajl-ruby](http://github.com/brianmario/yajl-ruby).
+
+Usage Examples
+--------------
+``` ruby
+require "rubygems"
+require "disqussion"
+
+# Get a user's details
+puts Disqussion.details("the88").url
+
+# Certain methods require authentication. To get your Disqus credentials,
+# register an app at http://disqus.com/api/applications/
+Disqussion.configure do |config|
+ config.api_key = YOUR_API_KEY
+ config.api_secret = YOUR_API_SECRET
+end
+
+# Read the most recent tweet in your home timeline
+puts Disqussion.home_timeline.first.text
+
+# Who's your most popular friend?
+puts Disqussion.friends.users.sort{|a, b| a.followers_count <=> b.followers_count}.reverse.first.name
+
+# Who's your most popular follower?
+puts Disqussion.followers.users.sort{|a, b| a.followers_count <=> b.followers_count}.reverse.first.name
+
+# Get your rate limit status
+puts Disqussion.rate_limit_status.remaining_hits.to_s + " Disqussion API request(s) remaining this hour"
+```
+
+Contributing
+------------
+In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project.
+
+Here are some ways *you* can contribute:
+
+* by using alpha, beta, and prerelease versions
+* by reporting bugs
+* by suggesting new features
+* by writing or editing documentation
+* by writing specifications
+* by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace)
+* by refactoring code
+* by closing [issues](http://github.com/jeremyvdw/disqussion/issues)
+* by reviewing patches
+
+All contributors will be added to the [HISTORY](https://github.com/jeremyvdw/disqussion/blob/master/HISTORY.mkd)
+file and will receive the respect and gratitude of the community.
+
+Submitting an Issue
+-------------------
+We use the [GitHub issue tracker](http://github.com/jeremyvdw/disqussion/issues) to track bugs and
+features. Before submitting a bug report or feature request, check to make sure it hasn't already
+been submitted. You can indicate support for an existing issuse by voting it up. When submitting a
+bug report, please include a [Gist](http://gist.github.com/) that includes a stack trace and any
+details that may be necessary to reproduce the bug, including your gem version, Ruby version, and
+operating system. Ideally, a bug report should include a pull request with failing specs.
+
+Submitting a Pull Request
+-------------------------
+1. Fork the project.
+2. Create a topic branch.
+3. Implement your feature or bug fix.
+4. Add documentation for your feature or bug fix.
+5. Run <tt>bundle exec rake doc:yard</tt>. If your changes are not 100% documented, go back to step 4.
+6. Add specs for your feature or bug fix.
+7. Run <tt>bundle exec rake spec</tt>. If your changes are not 100% covered, go back to step 6.
+8. Commit and push your changes.
+9. Submit a pull request. Please do not include changes to the gemspec, version, or history file. (If you want to create your own version for some reason, please do so in a separate commit.)
+
+Copyright
+---------
+Copyright (c) 2011 Jérémy Van de Wyngaert.
+See [LICENSE](https://github.com/jeremyvdw/disqussion/blob/master/LICENSE.mkd) for details.
23 Rakefile
@@ -0,0 +1,23 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec)
+
+task :test => :spec
+task :default => :spec
+
+namespace :doc do
+ require 'yard'
+ YARD::Rake::YardocTask.new do |task|
+ task.files = ['HISTORY.mkd', 'LICENSE.mkd', 'lib/**/*.rb']
+ task.options = [
+ '--protected',
+ '--output-dir', 'doc/yard',
+ '--tag', 'format:Supported formats',
+ '--tag', 'authenticated:Requires Authentication',
+ '--tag', 'rate_limited:Rate Limited',
+ '--markup', 'markdown',
+ ]
+ end
+end
38 disqussion.gemspec
@@ -0,0 +1,38 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('../lib/disqussion/version', __FILE__)
+
+Gem::Specification.new do |s|
+ s.name = "disqussion"
+ s.version = Disqussion::VERSION.dup
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Jérémy Van de Wyngaert"]
+ s.email = ['jeremyvdw@gmail.com']
+ s.homepage = 'https://github.com/jeremyvdw/disqussion'
+ s.summary = %q{Disqus API v3 wrapper}
+ s.description = %q{Disqus API v3 wrapper}
+
+ s.rubyforge_project = "disqussion"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency('json', '~> 1.5')
+ s.add_development_dependency('nokogiri', '~> 1.4')
+ s.add_development_dependency('maruku', '~> 0.6')
+ s.add_development_dependency('rake', '~> 0.8')
+ s.add_development_dependency('rspec', '~> 2.5')
+ s.add_development_dependency('simplecov', '~> 0.4')
+ s.add_development_dependency('webmock', '~> 1.6')
+ s.add_development_dependency('yard', '~> 0.6')
+ s.add_development_dependency('ZenTest', '~> 4.5')
+ s.add_development_dependency('ruby-debug19')
+
+ s.add_runtime_dependency('hashie', '~> 1.0.0')
+ s.add_runtime_dependency('faraday', '~> 0.6.1')
+ s.add_runtime_dependency('faraday_middleware', '~> 0.6.3')
+ s.add_runtime_dependency('multi_json', '~> 1.0.0')
+ s.add_runtime_dependency('rash', '~> 0.3.0')
+ s.add_runtime_dependency('simple_oauth', '~> 0.1.4')
+end
25 lib/disqussion.rb
@@ -0,0 +1,25 @@
+require 'disqussion/error'
+require 'disqussion/configuration'
+require 'disqussion/api'
+require 'disqussion/client'
+
+module Disqussion
+ extend Configuration
+
+ # Alias for Disqussion::Client.new
+ #
+ # @return [Disqussion::Client]
+ def self.client(options={})
+ Disqussion::Client.new(options)
+ end
+
+ # Delegate to Disqussion::Client
+ def self.method_missing(method, *args, &block)
+ return super unless client.respond_to?(method)
+ client.send(method, *args, &block)
+ end
+
+ def self.respond_to?(method, include_private = false)
+ client.respond_to?(method, include_private) || super(method, include_private)
+ end
+end
21 lib/disqussion/api.rb
@@ -0,0 +1,21 @@
+require 'disqussion/connection'
+require 'disqussion/request'
+
+module Disqussion
+ # @private
+ class API
+ # @private
+ attr_accessor *Configuration::VALID_OPTIONS_KEYS
+
+ # Creates a new API
+ def initialize(options={})
+ options = Disqussion.options.merge(options)
+ Configuration::VALID_OPTIONS_KEYS.each do |key|
+ send("#{key}=", options[key])
+ end
+ end
+
+ include Connection
+ include Request
+ end
+end
46 lib/disqussion/client.rb
@@ -0,0 +1,46 @@
+module Disqussion
+ # Wrapper for the Disqussion REST API
+ #
+ # @note All methods have been separated into client classes and follow the same grouping used in {http://disqus.com/api/docs/ the Disqus API Documentation}.
+ # @see http://docs.disqus.com/developers/api/
+ class Client < API
+ require 'disqussion/client/applications.rb'
+ require 'disqussion/client/blacklists.rb'
+ require 'disqussion/client/categories.rb'
+ require 'disqussion/client/exports.rb'
+ require 'disqussion/client/forums.rb'
+ require 'disqussion/client/imports.rb'
+ require 'disqussion/client/posts.rb'
+ require 'disqussion/client/reactions.rb'
+ require 'disqussion/client/reports.rb'
+ require 'disqussion/client/threads.rb'
+ require 'disqussion/client/trends.rb'
+ require 'disqussion/client/users.rb'
+ require 'disqussion/client/utils.rb'
+ require 'disqussion/client/whitelists.rb'
+
+ alias :api_endpoint :endpoint
+
+ include Disqussion::Client::Utils
+
+ ['applications',
+ 'blacklists',
+ 'categories',
+ 'exports',
+ 'forums',
+ 'imports',
+ 'posts',
+ 'reactions',
+ 'reports',
+ 'threads',
+ 'trends',
+ 'users',
+ 'whitelists'].each do |classname|
+ class_eval <<-END
+ def self.#{classname}
+ Disqussion::#{classname.capitalize}.new
+ end
+ END
+ end
+ end
+end
BIN  lib/disqussion/client/.DS_Store
Binary file not shown
19 lib/disqussion/client/applications.rb
@@ -0,0 +1,19 @@
+module Disqussion
+ class Applications < Client
+ # Returns the API usage per day for this application.
+ #
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: true
+ # @limited: false
+ # @return [Hashie::Rash] API usage per day for this application.
+ # @param application [Integer] Defaults to null
+ # @param days [Integer] Defaults to 30, Maximum length of 30
+ # @see http://disqus.com/api/3.0/applications/listUsage.json
+ def listUsage(*args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ get('applications/listUsage', options)
+ end
+ end
+end
4 lib/disqussion/client/blacklists.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Blacklists < Client
+ end
+end
4 lib/disqussion/client/categories.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Categories < Client
+ end
+end
4 lib/disqussion/client/exports.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Exports < Client
+ end
+end
110 lib/disqussion/client/forums.rb
@@ -0,0 +1,110 @@
+module Disqussion
+ class Forums < Client
+ # NOTE: to be implemented when debugged
+ # Creates a new forum.
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: true
+ # @limited: false
+ # @param website [String] Disqus website name.
+ # @param name [String] Forum name.
+ # @param short_name [String] Forum short name (aka forum id).
+ # @return {CURRENTLY BUGGED}
+ # @example Creates a new forum 'myforum'
+ # Disqus.create("myforum")
+ # @see: http://disqus.com/api/3.0/forums/create.json
+ def create(website, name, short_name)
+ response = get('forums/create', website, name, short_name)
+ end
+
+ # Returns forum details.
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: false
+ # @limited: false
+ # @param forum [String] Forum ID (aka short name).
+ # @return [Hashie::Rash] Details on the requested forum.
+ # @example Return extended information for forum 'myforum'
+ # Disqus.details("myforum")
+ # @see: http://disqus.com/api/3.0/forums/details.json
+ def details(forum)
+ response = get('forums/details', forum)
+ end
+
+ # Returns a list of categories within a forum.
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: false
+ # @limited: false
+ # @param forum [String] Forum ID (aka short name).
+ # @return [Hashie::Rash] Details on the requested list of categories.
+ # @param options [Hash] A customizable set of options.
+ # @option options [Datetime, Timestamp] :since_id. Unix timestamp (or ISO datetime standard). Defaults to null
+ # @option options [Integer] :cursor. Defaults to null
+ # @option options [Integer] :limit. Defaults to 25. Maximum length of 100
+ # @option options [String] :order. Defaults to "asc". Choices: asc, desc
+ # @example Return extended information for forum 'myforum'
+ # Disqus.listCategories("myforum", {:cursor => 10, :limit => 10, :order => 'asc'})
+ # @see: http://disqus.com/api/3.0/forums/details.json
+ def listCategories(forum, options = {})
+ response = get('forums/listCategories', forum, options)
+ end
+
+ # NOTE: to be implemented
+ # Returns a list of users active within a forum ordered by most likes received.
+ # @see: http://disqus.com/api/3.0/forums/listMostLikedUsers.json
+ def listMostLikedUsers(forum, options = {})
+ end
+
+ # Returns a list of posts within a forum.
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp, rss
+ # @authenticated: false
+ # @limited: false
+ # @param forum [String] Forum ID (aka short name).
+ # @return [Hashie::Rash] Details on the list of posts.
+ # @param options [Hash] A customizable set of options.
+ # @option options [Datetime, Timestamp] :since_id. Unix timestamp (or ISO datetime standard). Defaults to null
+ # @option options [Integer, String] :related. Allows multiple. Defaults to []. You may specify relations to include with your response. Choices: thread.
+ # @option options [Integer] :cursor. Defaults to null
+ # @option options [Integer] :limit. Defaults to 25. Maximum length of 100
+ # @option options [Integer] :query. Defaults to null.
+ # @option options [String, Array] :include allows multiple. Defaults to ["approved"]. Choices: unapproved, approved, spam, deleted, flagged
+ # @option options [String] :order. Defaults to "asc". Choices: asc, desc
+ # @example Return list of (all) posts for forum 'myforum', including related threads
+ # Disqus.listPosts("myforum", {:related => ["thread"], :include => ["spam", "deleted", "flagged"]})
+ # @see: http://disqus.com/api/3.0/forums/listPosts.json
+ def listPosts(forum, options = {})
+ response = get('forums/listPosts', forum, options)
+ end
+
+ # Returns a list of threads within a forum.
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp, rss
+ # @authenticated: false
+ # @limited: false
+ # @param forum [String] Forum ID (aka short name).
+ # @return [Hashie::Rash] Details on the list of posts.
+ # @param options [Hash] A customizable set of options.
+ # @option options [Integer, String, Array] :thread. Allows multiple. Defaults to null. Looks up a thread by ID. You may pass use the 'ident' or 'link' query types instead of an ID by including 'forum'
+ # @option options [Datetime, Timestamp] :since_id. Unix timestamp (or ISO datetime standard). Defaults to null
+ # @option options [Integer, String] :related. Allows multiple. Defaults to []. You may specify relations to include with your response. Choices: forum, author.
+ # @option options [Integer] :cursor. Defaults to null
+ # @option options [Integer] :limit. Defaults to 25. Maximum length of 100
+ # @option options [String, Array] :include allows multiple. Defaults to ["open", "close"]. Choices: open, closed, killed.
+ # @option options [String] :order. Defaults to "desc". Choices: asc, desc
+ # @example Return list of (all) threads for forum 'myforum', including closed ones and related forums.
+ # Disqus.listThreads("myforum", {:related => ["forum"], :include => ["close"]})
+ # @see: http://disqus.com/api/3.0/forums/listThreads.json
+ def listThreads(forum, options = {})
+ # NOTE: extract :thread to tie it up
+ response = get('forums/listThreads', forum, options)
+ end
+
+ end
+end
4 lib/disqussion/client/imports.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Imports < Client
+ end
+end
4 lib/disqussion/client/posts.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Posts < Client
+ end
+end
4 lib/disqussion/client/reactions.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Reactions < Client
+ end
+end
4 lib/disqussion/client/reports.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Reports < Client
+ end
+end
4 lib/disqussion/client/threads.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Threads < Client
+ end
+end
5 lib/disqussion/client/trends.rb
@@ -0,0 +1,5 @@
+module Disqussion
+ class Trends < Client
+
+ end
+end
103 lib/disqussion/client/users.rb
@@ -0,0 +1,103 @@
+module Disqussion
+ class Users < Client
+ # Returns details of a user
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: false
+ # @limited: false
+ # @param user [Integer, String] A Disqus user ID or screen name.
+ # @return [Hashie::Rash] Details on the requested user.
+ # @example Return extended information for 'the88'
+ # Disqus.user("the88")
+ # Disqus.user(6138058) # Same as above
+ # @see: http://disqus.com/api/3.0/users/details.json
+ def details(*args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ user = args.first
+ merge_user_into_options!(user, options)
+ response = get('users/details', options)
+ end
+
+ # Follow a user
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: true
+ # @limited: false
+ # @param target [Integer, String] A Disqus user ID or screen name.
+ # @return [Hashie::Rash] Details on the requested user.
+ # @example Return extended information for 'the88'
+ # Disqus.follow("the88")
+ # Disqus.follow(6138058) # Same as above
+ # @see: http://disqus.com/api/3.0/users/details.json
+ def follow(*args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ target = args.first
+ merge_target_into_options!(target, options)
+ response = post('users/follow', options)
+ end
+
+ # Returns a list of forums a user has been active on.
+ def listActiveForums
+
+ end
+
+ # BETA
+ # Returns a list of threads a user has participated in sorted by last activity.
+ def listActiveThreads
+
+ end
+
+ # BETA
+ # Returns a list of various activity types made by the user.
+ def listActivity
+
+ end
+
+ # BETA
+ # Returns a list of users a user is being followed by.
+ def listFollowers
+
+ end
+
+ # BETA
+ # Returns a list of users a user is following.
+ def listFollowing
+
+ end
+
+ # Returns a list of forums a user owns.
+ def listForums
+
+ end
+
+ # BETA
+ # Returns a list of forums a user has been active on recenty, sorted by the user's activity.
+ def listMostActiveForums
+
+ end
+
+ # Returns a list of posts made by the user.
+ def listPosts
+
+ end
+
+ # Unfollow a user
+ # @accessibility: public key, secret key
+ # @methods: GET
+ # @format: json, jsonp
+ # @authenticated: true
+ # @limited: false
+ # @param user [Integer, String] A Disqus user ID or screen name.
+ # @return [Hashie::Rash] Details on the requested user.
+ # @example Return extended information for 'the88'
+ # Disqus.unfollow("the88")
+ # Disqus.unfollow(6138058) # Same as above
+ # @see: http://disqus.com/api/3.0/users/details.json
+ def unfollow
+ merge_user_into_options!(user, options)
+ response = post('users/unfollow', options)
+ end
+ end
+end
90 lib/disqussion/client/utils.rb
@@ -0,0 +1,90 @@
+# -*- encoding: utf-8 -*-
+module Disqussion
+ class Client
+ # @private
+ module Utils
+ private
+
+ # Take a single user ID or screen name and merge it into an options hash with the correct key
+ #
+ # @param user_id_or_username [Integer, String] A Disqus user ID or username.
+ # @param options [Hash] A customizable set of options.
+ # @return [Hash]
+ def merge_user_into_options!(user_id_or_username, options={})
+ case user_id_or_username
+ when Fixnum
+ options[:user] = user_id_or_username
+ when String
+ options[:"user:username"] = user_id_or_username
+ end
+ options
+ end
+
+ # Take a single user ID or screen name and merge it into an options hash with the correct key
+ #
+ # @param user_id_or_username [Integer, String] A Disqus user ID or username.
+ # @param options [Hash] A customizable set of options.
+ # @return [Hash]
+ def merge_target_into_options!(user_id_or_username, options={})
+ case user_id_or_username
+ when Fixnum
+ options[:target] = user_id_or_username
+ when String
+ options[:"target:username"] = user_id_or_username
+ end
+ options
+ end
+
+ # Take a multiple user IDs and screen names and merge them into an options hash with the correct keys
+ #
+ # @param users_id_or_usernames [Array] An array of Disqus user IDs or usernames.
+ # @param options [Hash] A customizable set of options.
+ # @return [Hash]
+ def merge_users_into_options!(user_ids_or_usernames, options={})
+ user_ids, usernames = [], []
+ user_ids_or_usernames.flatten.each do |user_id_or_username|
+ case user_id_or_username
+ when Fixnum
+ user_ids << user_id_or_username
+ when String
+ usernames << user_id_or_username
+ end
+ end
+ options[:user_id] = user_ids.join(',') unless user_ids.empty?
+ options[:username] = usernames.join(',') unless usernames.empty?
+ options
+ end
+
+ # Take a single owner ID or owner screen name and merge it into an options hash with the correct key
+ # (for Disqus API endpoints that want :owner_id and :owner_username)
+ #
+ # @param owner_id_or_owner_username [Integer, String] A Disqus user ID or username.
+ # @param options [Hash] A customizable set of options.
+ # @return [Hash]
+ def merge_owner_into_options!(owner_id_or_owner_username, options={})
+ case owner_id_or_owner_username
+ when Fixnum
+ options[:owner_id] = owner_id_or_owner_username
+ when String
+ options[:owner_username] = owner_id_or_owner_username
+ end
+ options
+ end
+
+ # Take a single list ID or slug and merge it into an options hash with the correct key
+ #
+ # @param list_id_or_slug [Integer, String] A Disqus list ID or slug.
+ # @param options [Hash] A customizable set of options.
+ # @return [Hash]
+ def merge_list_into_options!(list_id_or_username, options={})
+ case list_id_or_username
+ when Fixnum
+ options[:list_id] = list_id_or_username
+ when String
+ options[:slug] = list_id_or_username
+ end
+ options
+ end
+ end
+ end
+end
4 lib/disqussion/client/whitelists.rb
@@ -0,0 +1,4 @@
+module Disqussion
+ class Whitelists < Client
+ end
+end
121 lib/disqussion/configuration.rb
@@ -0,0 +1,121 @@
+require 'faraday'
+require 'disqussion/version'
+
+module Disqussion
+ # Defines constants and methods related to configuration
+ module Configuration
+ # An array of valid keys in the options hash when configuring a {Disqussion::API}
+ VALID_OPTIONS_KEYS = [
+ :adapter,
+ :endpoint,
+ :api_key,
+ :api_secret,
+ :developer,
+ :container_id,
+ :avatar_size,
+ :color,
+ :default_tab,
+ :hide_avatars,
+ :hide_mods,
+ :num_items,
+ :show_powered_by,
+ :orientation,
+ :format,
+ :proxy,
+ :user_agent].freeze
+
+ # An array of valid request/response formats
+ #
+ # @note only json for now
+ VALID_FORMATS = [:json].freeze
+
+ # The adapter that will be used to connect if none is set
+ #
+ # @note The default faraday adapter is Net::HTTP.
+ DEFAULT_ADAPTER = if defined?(EventMachine) && EM.reactor_running?
+ EMSynchrony
+ else
+ Faraday.default_adapter
+ end
+
+ # By default, don't set an application key
+ DEFAULT_API_KEY = nil
+
+ # By default, don't set an application secret
+ DEFAULT_API_SECRET = nil
+
+ # The endpoint that will be used to connect if none is set
+ #
+ # @note This is configurable in case you want to use HTTP instead of HTTPS, specify a different API version, or use a Disqussion-compatible endpoint.
+ # @see http://status.net/wiki/Disqussion-compatible_API
+ # @see http://en.blog.wordpress.com/2009/12/12/disqussion-api/
+ # @see http://staff.tumblr.com/post/287703110/api
+ # @see http://developer.typepad.com/typepad-disqussion-api/disqussion-api.html
+ DEFAULT_API_VERSION = '3.0'.freeze
+ DEFAULT_ENDPOINT = "https://disqus.com/api/#{DEFAULT_API_VERSION}/".freeze
+
+ # The response format appended to the path and sent in the 'Accept' header if none is set
+ #
+ # @note JSON is preferred over XML because it is more concise and faster to parse.
+ DEFAULT_FORMAT = :json
+
+ # By default, don't use a proxy server
+ DEFAULT_PROXY = nil
+
+ # The user agent that will be sent to the API endpoint if none is set
+ DEFAULT_USER_AGENT = "Disqussion Ruby Gem #{Disqussion::VERSION}".freeze
+
+ DEFAULT_DEVELOPER = false
+ DEFAULT_CONTAINER_ID = 'disqus_thread'
+ DEFAULT_AVATAR_SIZE = 48
+ DEFAULT_COLOR = "grey"
+ DEFAULT_DEFAULT_TAB = "popular"
+ DEFAULT_HIDE_AVATARS = false
+ DEFAULT_HIDE_MODS = true
+ DEFAULT_NUM_ITEMS = 15
+ DEFAULT_SHOW_POWERED_BY = true
+ DEFAULT_ORIENTATION = "horizontal"
+
+ # @private
+ attr_accessor *VALID_OPTIONS_KEYS
+
+ # When this module is extended, set all configuration options to their default values
+ def self.extended(base)
+ base.reset
+ end
+
+ # Convenience method to allow configuration options to be set in a block
+ def configure
+ yield self
+ end
+
+ # Create a hash of options and their values
+ def options
+ options = {}
+ VALID_OPTIONS_KEYS.each{|k| options[k] = send(k) }
+ options
+ end
+
+ # Reset all configuration options to defaults
+ def reset
+ self.adapter = DEFAULT_ADAPTER
+ self.endpoint = DEFAULT_ENDPOINT
+ self.api_key = DEFAULT_API_KEY
+ self.api_secret = DEFAULT_API_SECRET
+ self.developer = DEFAULT_DEVELOPER
+ self.container_id = DEFAULT_CONTAINER_ID
+ self.avatar_size = DEFAULT_AVATAR_SIZE
+ self.color = DEFAULT_COLOR
+ self.default_tab = DEFAULT_DEFAULT_TAB
+ self.hide_avatars = DEFAULT_HIDE_AVATARS
+ self.hide_mods = DEFAULT_HIDE_MODS
+ self.num_items = DEFAULT_NUM_ITEMS
+ self.show_powered_by = DEFAULT_SHOW_POWERED_BY
+ self.orientation = DEFAULT_ORIENTATION
+ self.format = DEFAULT_FORMAT
+ self.proxy = DEFAULT_PROXY
+ self.user_agent = DEFAULT_USER_AGENT
+ self
+ end
+ end
+end
36 lib/disqussion/connection.rb
@@ -0,0 +1,36 @@
+require 'faraday_middleware'
+require 'faraday/response/raise_http_4xx'
+require 'faraday/response/raise_http_5xx'
+
+module Disqussion
+ # @private
+ module Connection
+ private
+
+ def connection(raw=false)
+ options = {
+ :headers => {'Accept' => "application/#{format}", 'User-Agent' => user_agent},
+ :proxy => proxy,
+ :ssl => {:verify => false},
+ :url => api_endpoint,
+ }
+
+ Faraday.new(options) do |builder|
+ builder.use Faraday::Request::Multipart
+ builder.use Faraday::Request::UrlEncoded
+ builder.use Faraday::Response::RaiseHttp4xx
+ builder.use Faraday::Response::Rashify unless raw
+ unless raw
+ case format.to_s.downcase
+ when 'json'
+ builder.use Faraday::Response::ParseJson
+ when 'xml'
+ builder.use Faraday::Response::ParseXml
+ end
+ end
+ builder.use Faraday::Response::RaiseHttp5xx
+ builder.adapter(adapter)
+ end
+ end
+ end
+end
51 lib/disqussion/error.rb
@@ -0,0 +1,51 @@
+module Disqussion
+ # Custom error class for rescuing from all Disqus errors
+ class Error < StandardError
+ attr_reader :http_headers
+
+ def initialize(message, http_headers)
+ @http_headers = Hash[http_headers]
+ super message
+ end
+
+ def ratelimit_reset
+ Time.at(@http_headers.values_at('x-ratelimit-reset', 'X-RateLimit-Reset').detect{|value| value}.to_i)
+ end
+
+ def ratelimit_limit
+ @http_headers.values_at('x-ratelimit-limit', 'X-RateLimit-Limit').detect{|value| value}.to_i
+ end
+
+ def ratelimit_remaining
+ @http_headers.values_at('x-ratelimit-remaining', 'X-RateLimit-Remaining').detect{|value| value}.to_i
+ end
+
+ def retry_after
+ [(ratelimit_reset - Time.now).ceil, 0].max
+ end
+ end
+
+ # Raised when Disqus returns the HTTP status code 400
+ class BadRequest < Error; end
+
+ # Raised when Disqus returns the HTTP status code 401
+ class Unauthorized < Error; end
+
+ # Raised when Disqus returns the HTTP status code 403
+ class Forbidden < Error; end
+
+ # Raised when Disqus returns the HTTP status code 404
+ class NotFound < Error; end
+
+ # Raised when Disqus returns the HTTP status code 406
+ class NotAcceptable < Error; end
+
+ # Raised when Disqus returns the HTTP status code 500
+ class InternalServerError < Error; end
+
+ # Raised when Disqus returns the HTTP status code 502
+ class BadGateway < Error; end
+
+ # Raised when Disqus returns the HTTP status code 503
+ class ServiceUnavailable < Error; end
+end
38 lib/disqussion/request.rb
@@ -0,0 +1,38 @@
+module Disqussion
+ # Defines HTTP request methods
+ module Request
+ # Perform an HTTP GET request
+ def get(path, options={}, raw=false)
+ request(:get, path, options, raw)
+ end
+
+ # Perform an HTTP POST request
+ def post(path, options={}, raw=false)
+ request(:post, path, options, raw)
+ end
+
+ private
+
+ # Perform an HTTP request
+ def request(method, path, options, raw=false)
+ # identify our request
+ # @see: http://docs.disqus.com/help/52/
+ options ||= {}
+ options.merge!(:api_secret => self.api_secret) unless options.has_key?(:api_secret)
+ response = connection(raw).send(method) do |request|
+ case method
+ when :get
+ request.url(formatted_path(path), options)
+ when :post
+ request.path = formatted_path(path)
+ request.body = options unless options.empty?
+ end
+ end
+ raw ? response : response.body
+ end
+
+ def formatted_path(path)
+ [path, format].compact.join('.')
+ end
+ end
+end
3  lib/disqussion/version.rb
@@ -0,0 +1,3 @@
+module Disqussion
+ VERSION = '0.0.1'.freeze unless defined?(::Disqussion::VERSION)
+end
9 lib/disqussion/view_helpers.rb
@@ -0,0 +1,9 @@
+# Shortcuts to access the widgets as simple functions as opposed to using
+# their full qualified names.
+%w[combo comment_counts popular_threads recent_comments thread top_commenters].each do |method|
+ eval(<<-EOM)
+ def disqus_#{method}(options = {})
+ Disqus::Widget.#{method}(options)
+ end
+ EOM
+end
183 lib/disqussion/widget.rb
@@ -0,0 +1,183 @@
+module EventMachine
+ module Disqus
+ # Disqus Widget generator.
+ #
+ # All of the methods accept various options, and "account" is required for
+ # all of them. You can avoid having to pass in the account each time by
+ # setting it in the defaults like this:
+ #
+ # Disqus::defaults[:account] = "my_account"
+ class Widget
+
+ VALID_COLORS = ['blue', 'grey', 'green', 'red', 'orange']
+ VALID_NUM_ITEMS = 5..20
+ VALID_DEFAULT_TABS = ['people', 'recent', 'popular']
+ VALID_AVATAR_SIZES = [24, 32, 48, 92, 128]
+ VALID_ORIENTATIONS = ['horizontal', 'vertical']
+
+ ROOT_PATH = 'http://disqus.com/forums/%s/'
+ THREAD = ROOT_PATH + 'embed.js'
+ COMBO = ROOT_PATH + 'combination_widget.js?num_items=%d&color=%s&default_tab=%s'
+ RECENT = ROOT_PATH + 'recent_comments_widget.js?num_items=%d&avatar_size=%d'
+ POPULAR = ROOT_PATH + 'popular_threads_widget.js?num_items=%d'
+ TOP = ROOT_PATH + 'top_commenters_widget.js?num_items=%d&avatar_size=%d&orientation=%s'
+ class << self
+
+ # Show the main Disqus thread widget.
+ # Options:
+ # * <tt>account:</tt> Your Discus account (required).
+ def thread(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ opts[:view_thread_text] ||= "View the discussion thread"
+ validate_opts!(opts)
+ s = ''
+ if opts[:developer]
+ s << '<script type="text/javascript">var disqus_developer = 1;</script>'
+ end
+ s << '<div id="disqus_thread"></div>'
+ s << '<script type="text/javascript" src="' + THREAD + '"></script>'
+ s << '<noscript><a href="http://%s.disqus.com/?url=ref">'
+ s << opts[:view_thread_text]
+ s << '</a></noscript>'
+ if opts[:show_powered_by]
+ s << '<a href="http://disqus.com" class="dsq-brlink">blog comments '
+ s << 'powered by <span class="logo-disqus">Disqus</span></a>'
+ end
+ s % [opts[:account], opts[:account]]
+ end
+
+ # Loads Javascript to show the number of comments for the page.
+ #
+ # The Javascript sets the inner html to the comment count for any links
+ # on the page that have the anchor "disqus_thread". For example, "View
+ # Comments" below would be replaced by "1 comment" or "23 comments" etc.
+ #
+ # <a href="http://my.website/article-permalink#disqus_thread">View Comments</a>
+ # <a href="http://my.website/different-permalink#disqus_thread">View Comments</a>
+ # Options:
+ # * <tt>account:</tt> Your Discus account (required).
+ def comment_counts(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ validate_opts!(opts)
+ s = <<-WHIMPER
+ <script type="text/javascript">
+ //<![CDATA[
+ (function() {
+ var links = document.getElementsByTagName('a');
+ var query = '?';
+ for(var i = 0; i < links.length; i++) {
+ if(links[i].href.indexOf('#disqus_thread') >= 0) {
+ query += 'url' + i + '=' + encodeURIComponent(links[i].href) + '&';
+ }
+ }
+ document.write('<' + 'script type="text/javascript" src="#{ROOT_PATH}get_num_replies.js' + query + '"></' + 'script>');
+ })();
+ //]]>
+ </script>
+ WHIMPER
+ s % opts[:account]
+ end
+
+ # Show the top commenters Disqus thread widget.
+ # Options:
+ # * <tt>account:</tt> Your Discus account (required).
+ # * <tt>header:</tt> HTML snipper with header (default h2) tag and text.
+ # * <tt>show_powered_by:</tt> Show or hide the powered by Disqus text.
+ # * <tt>num_items:</tt>: How many items to show.
+ # * <tt>hide_mods:</tt> Don't show moderators.
+ # * <tt>hide_avatars:</tt> Don't show avatars.
+ # * <tt>avatar_size:</tt> Avatar size.
+ def top_commenters(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ opts[:header] ||= '<h2 class="dsq-widget-title">Top Commenters</h2>'
+ validate_opts!(opts)
+ s = '<div id="dsq-topcommenters" class="dsq-widget">'
+ s << opts[:header]
+ s << '<script type="text/javascript" src="'
+ s << TOP
+ s << '&hide_avatars=1' if opts[:hide_avatars]
+ s << '&hide_mods=1' if opts[:hide_mods]
+ s << '"></script>'
+ s << '</div>'
+ if opts[:show_powered_by]
+ s << '<a href="http://disqus.com">Powered by Disqus</a>'
+ end
+ s % [opts[:account], opts[:num_items], opts[:avatar_size], opts[:orientation]]
+ end
+
+ # Show the popular threads Disqus widget.
+ # Options:
+ # * <tt>account:</tt> Your Discus account (required).
+ # * <tt>header:</tt> HTML snipper with header (default h2) tag and text.
+ # * <tt>num_items:</tt>: How many items to show.
+ # * <tt>hide_mods:</tt> Don't show moderators.
+ def popular_threads(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ opts[:header] ||= '<h2 class="dsq-widget-title">Popular Threads</h2>'
+ validate_opts!(opts)
+ s = '<div id="dsq-popthreads" class="dsq-widget">'
+ s << opts[:header]
+ s << '<script type="text/javascript" src="'
+ s << POPULAR
+ s << '&hide_mods=1' if opts[:hide_mods]
+ s << '"></script>'
+ s << '</div>'
+ s << '<a href="http://disqus.com">Powered by Disqus</a>' if opts[:show_powered_by]
+ s % [opts[:account], opts[:num_items]]
+ end
+
+ # Show the recent comments Disqus widget.
+ # Options:
+ # * <tt>account:</tt> Your Discus account (required).
+ # * <tt>header:</tt> HTML snipper with header (default h2) tag and text.
+ # * <tt>num_items:</tt>: How many items to show.
+ # * <tt>hide_avatars:</tt> Don't show avatars.
+ # * <tt>avatar_size:</tt> Avatar size.
+ def recent_comments(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ opts[:header] ||= '<h2 class="dsq-widget-title">Recent Comments</h2>'
+ validate_opts!(opts)
+ s = '<div id="dsq-recentcomments" class="dsq-widget">'
+ s << opts[:header]
+ s << '<script type="text/javascript" src="'
+ s << RECENT
+ s << '&hide_avatars=1' if opts[:hide_avatars]
+ s << '"></script>'
+ s << '</div>'
+ if opts[:show_powered_by]
+ s << '<a href="http://disqus.com">Powered by Disqus</a>'
+ end
+ s % [opts[:account], opts[:num_items], opts[:avatar_size]]
+ end
+
+ # Show the Disqus combo widget. This is a three-tabbed box with links
+ # popular threads, top posters, and recent threads.
+ # Options:
+ # * <tt>:account:</tt> Your Discus account (required).
+ # * <tt>:num_items:</tt> How many items to show.
+ # * <tt>:hide_mods:</tt> Don't show moderators.
+ # * <tt>:default_tab:</tt> Should be 'people', 'recent', or 'popular'.
+ def combo(opts = {})
+ opts = Disqus::defaults.merge(opts)
+ validate_opts!(opts)
+ s = '<script type="text/javascript" src="'
+ s << COMBO
+ s << '&hide_mods=1' if opts[:hide_mods]
+ s << '"></script>'
+ s % [opts[:account], opts[:num_items], opts[:color], opts[:default_tab]]
+ end
+
+ private
+
+ def validate_opts!(opts)
+ raise ArgumentError.new("You must specify an :account") if !opts[:account]
+ raise ArgumentError.new("Invalid color") if opts[:color] && !VALID_COLORS.include?(opts[:color])
+ raise ArgumentError.new("Invalid num_items") if opts[:num_items] && !VALID_NUM_ITEMS.include?(opts[:num_items])
+ raise ArgumentError.new("Invalid default_tab") if opts[:default_tab] && !VALID_DEFAULT_TABS.include?(opts[:default_tab])
+ raise ArgumentError.new("Invalid avatar size") if opts[:avatar_size] && !VALID_AVATAR_SIZES.include?(opts[:avatar_size])
+ raise ArgumentError.new("Invalid orientation") if opts[:orientation] && !VALID_ORIENTATIONS.include?(opts[:orientation])
+ end
+ end
+ end
+ end
+end
41 lib/faraday/response/raise_http_4xx.rb
@@ -0,0 +1,41 @@
+require 'faraday'
+
+# @private
+module Faraday
+ # @private
+ class Response::RaiseHttp4xx < Response::Middleware
+ def on_complete(env)
+ case env[:status].to_i
+ when 400
+ raise Disqussion::BadRequest.new(error_message(env), env[:response_headers])
+ when 401
+ raise Disqussion::Unauthorized.new(error_message(env), env[:response_headers])
+ when 403
+ raise Disqussion::Forbidden.new(error_message(env), env[:response_headers])
+ when 404
+ raise Disqussion::NotFound.new(error_message(env), env[:response_headers])
+ end
+ end
+
+ private
+
+ def error_message(env)
+ "#{env[:method].to_s.upcase} #{env[:url].to_s}: #{env[:status]}#{error_body(env[:body])}"
+ end
+
+ def error_body(body)
+ if body.nil?
+ nil
+ elsif body['error']
+ ": #{body['error']}"
+ elsif body['errors']
+ first = body['errors'].to_a.first
+ if first.kind_of? Hash
+ ": #{first['message'].chomp}"
+ else
+ ": #{first.chomp}"
+ end
+ end
+ end
+ end
+end
20 lib/faraday/response/raise_http_5xx.rb
@@ -0,0 +1,20 @@
+require 'faraday'
+
+# @private
+module Faraday
+ # @private
+ class Response::RaiseHttp5xx < Response::Middleware
+ def on_complete(env)
+ case env[:status].to_i
+ when 500
+ raise Disqussion::InternalServerError.new(error_message(env, "Something is technically wrong."), env[:response_headers])
+ end
+ end
+
+ private
+
+ def error_message(env, body=nil)
+ "#{env[:method].to_s.upcase} #{env[:url].to_s}: #{[env[:status].to_s + ':', body].compact.join(' ')} Check http://status.disqus.com/ for updates on the status of the Disqus service."
+ end
+ end
+end
63 spec/disqussion/api_spec.rb
@@ -0,0 +1,63 @@
+require 'helper'
+
+describe Disqussion::API do
+ before do
+ @keys = Disqussion::Configuration::VALID_OPTIONS_KEYS
+ end
+
+ context "with module configuration" do
+ before do
+ Disqussion.configure do |config|
+ @keys.each do |key|
+ config.send("#{key}=", key)
+ end
+ end
+ end
+
+ after do
+ Disqussion.reset
+ end
+
+ it "should inherit module configuration" do
+ api = Disqussion::API.new
+ @keys.each do |key|
+ api.send(key).should == key
+ end
+ end
+
+ context "with class configuration" do
+ before do
+ @configuration = {
+ :api_key => 'key',
+ :api_secret => 'secret',
+ :adapter => :typhoeus,
+ :endpoint => 'http://disqus.com/api',
+ :format => :json,
+ :proxy => 'http://the88:bar@proxy.example.com:8080',
+ :user_agent => 'Custom User Agent',
+ }
+ end
+
+ context "during initialization" do
+ it "should override module configuration" do
+ api = Disqussion::API.new(@configuration)
+ @keys.each do |key|
+ api.send(key).should == @configuration[key] if @configuration[key]
+ end
+ end
+ end
+
+ context "after initilization" do
+ it "should override module configuration after initialization" do
+ api = Disqussion::API.new
+ @configuration.each do |key, value|
+ api.send("#{key}=", value)
+ end
+ @keys.each do |key|
+ api.send(key).should == @configuration[key] if @configuration[key]
+ end
+ end
+ end
+ end
+ end
+end
BIN  spec/disqussion/client/.DS_Store
Binary file not shown
24 spec/disqussion/client/applications_spec.rb
@@ -0,0 +1,24 @@
+require 'helper'
+
+describe Disqussion::Applications do
+ Disqussion::Configuration::VALID_FORMATS.each do |format|
+ context ".new(:format => '#{format}')" do
+ before do
+ @client = Disqussion::Client.applications
+ end
+
+ context "listUsage" do
+ before do
+ stub_get("applications/listUsage.json").
+ to_return(:body => fixture("applications/listUsage.json"), :headers => {:content_type => "application/json; charset=utf-8"})
+ end
+
+ it "returns the API usage per day for this application." do
+ @client.listUsage
+ a_get("applications/listUsage.json").should have_been_made
+ end
+ end
+
+ end
+ end
+end
21 spec/disqussion/client/forums_spec.rb
@@ -0,0 +1,21 @@
+require 'helper'
+
+describe Disqussion::Forums do
+ Disqussion::Configuration::VALID_FORMATS.each do |format|
+ context ".new(:format => '#{format}')" do
+ before do
+ @client = Disqussion::Client.forums
+ end
+
+ describe ".create" do
+ end
+
+ describe ".details" do
+ end
+
+ describe ".list_categories" do
+ end
+
+ end
+ end
+end
33 spec/disqussion/client/users_spec.rb
@@ -0,0 +1,33 @@
+require 'helper'
+
+describe Disqussion::Users do
+ Disqussion::Configuration::VALID_FORMATS.each do |format|
+ context ".new(:format => '#{format}')" do
+ before do
+ @client = Disqussion::Client.users
+ end
+
+ describe ".details" do
+ it "get the correct resource" do
+ stub_get("users/details.json").
+ to_return(:body => fixture("users/details.json"), :headers => {:content_type => "application/json; charset=utf-8"})
+
+ @client.details
+
+ a_get("users/details.json").should have_been_made
+ end
+ end
+
+ describe ".follow" do
+ it "get the correct resource" do
+ stub_post("users/follow.json", :body => { :target => "12345678" }).
+ to_return(:body => fixture("users/follow.json"), :headers => {:content_type => "application/json; charset=utf-8"})
+
+ @client.follow(12345678)
+
+ a_post("users/follow.json", :body => { :target => "12345678"}).should have_been_made
+ end
+ end
+ end
+ end
+end
15 spec/disqussion/client_spec.rb
@@ -0,0 +1,15 @@
+require 'helper'
+
+describe Disqussion::Client do
+ context ".new" do
+ before do
+ @client = Disqussion::Client.new
+ end
+
+ it "connect using the endpoint configuration" do
+ endpoint = URI.parse(@client.api_endpoint)
+ connection = @client.send(:connection).build_url(nil).to_s
+ connection.should == endpoint.to_s
+ end
+ end
+end
101 spec/disqussion_spec.rb
@@ -0,0 +1,101 @@
+require 'helper'
+
+describe Disqussion do
+ after do
+ Disqussion.reset
+ end
+
+ context "when delegating to a client" do
+ # before do
+ # stub_get("users/details.json").
+ # with(:query => {:"user:username" => "the88"}).
+ # to_return(:body => fixture("statuses.json"), :headers => {:content_type => "application/json; charset=utf-8"})
+ # end
+ #
+ # it "should get the correct resource" do
+ # Disqussion.user_timeline('the88')
+ # a_get("users/details.json").
+ # with(:query => {:"user:username" => "the88"}).
+ # should have_been_made
+ # end
+ #
+ # it "should return the same results as a client" do
+ # Disqussion.user_timeline('the88').should == Disqussion::Client.new.user_timeline('the88')
+ # end
+ end
+
+ describe '.respond_to?' do
+ it 'takes an optional include private argument' do
+ Disqussion.respond_to?(:client, true).should be_true
+ end
+ end
+
+ describe ".client" do
+ it "should be a Disqussion::Client" do
+ Disqussion.client.should be_a Disqussion::Client
+ end
+ end
+
+ describe ".adapter" do
+ it "should return the default adapter" do
+ Disqussion.adapter.should == Disqussion::Configuration::DEFAULT_ADAPTER
+ end
+ end
+
+ describe ".adapter=" do
+ it "should set the adapter" do
+ Disqussion.adapter = :typhoeus
+ Disqussion.adapter.should == :typhoeus
+ end
+ end
+
+ describe ".endpoint" do
+ it "should return the default endpoint" do
+ Disqussion.endpoint.should == Disqussion::Configuration::DEFAULT_ENDPOINT
+ end
+ end
+
+ describe ".endpoint=" do
+ it "should set the endpoint" do
+ Disqussion.endpoint = 'http://tumblr.com/'
+ Disqussion.endpoint.should == 'http://tumblr.com/'
+ end
+ end
+
+ describe ".format" do
+ it "should return the default format" do
+ Disqussion.format.should == Disqussion::Configuration::DEFAULT_FORMAT
+ end
+ end
+
+ describe ".format=" do
+ it "should set the format" do
+ Disqussion.format = 'xml'
+ Disqussion.format.should == 'xml'
+ end
+ end
+
+ describe ".user_agent" do
+ it "should return the default user agent" do
+ Disqussion.user_agent.should == Disqussion::Configuration::DEFAULT_USER_AGENT
+ end
+ end
+
+ describe ".user_agent=" do
+ it "should set the user_agent" do
+ Disqussion.user_agent = 'Custom User Agent'
+ Disqussion.user_agent.should == 'Custom User Agent'
+ end
+ end
+
+ describe ".configure" do
+ Disqussion::Configuration::VALID_OPTIONS_KEYS.each do |key|
+ it "should set the #{key}" do
+ Disqussion.configure do |config|
+ config.send("#{key}=", key)
+ Disqussion.send(key).should == key
+ end
+ end
+ end
+ end
+end
30 spec/faraday/response_spec.rb
@@ -0,0 +1,30 @@
+require 'helper'
+
+describe Faraday::Response do
+ before do
+ @client = Disqussion::Client.users
+ end
+
+ {
+ 400 => Disqussion::BadRequest,
+ 401 => Disqussion::Unauthorized,
+ 403 => Disqussion::Forbidden,
+ 404 => Disqussion::NotFound,
+ 500 => Disqussion::InternalServerError,
+ }.each do |status, exception|
+ context "when HTTP status is #{status}" do
+
+ before do
+ stub_get('users/details.json').
+ with(:query => {:"user:username" => 'the88'}).
+ to_return(:status => status)
+ end
+
+ it "should raise #{exception.name} error" do
+ lambda do
+ @client.details('the88')
+ end.should raise_error(exception)
+ end
+ end
+ end
+end
129 spec/fixtures/applications/listUsage.json
@@ -0,0 +1,129 @@
+{
+ "code": 0,
+ "response": [
+ [
+ "2011-04-05T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-06T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-07T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-08T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-09T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-10T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-11T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-12T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-13T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-14T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-15T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-16T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-17T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-18T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-19T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-20T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-21T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-22T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-23T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-24T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-25T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-26T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-27T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-28T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-29T00:00:00",
+ 0
+ ],
+ [
+ "2011-04-30T00:00:00",
+ 0
+ ],
+ [
+ "2011-05-01T00:00:00",
+ 0
+ ],
+ [
+ "2011-05-02T00:00:00",
+ 0
+ ],
+ [
+ "2011-05-03T00:00:00",
+ 0
+ ],
+ [
+ "2011-05-04T00:00:00",
+ 3
+ ],
+ [
+ "2011-05-05T00:00:00",
+ 9
+ ]
+ ]
+}
12 spec/fixtures/forums/details.json
@@ -0,0 +1,12 @@
+{
+ "code": 0,
+ "response": {
+ "id": "test",
+ "name": "test forum",
+ "founder": "6138058",
+ "favicon": {
+ "permalink": "http://disqus.com/api/forums/favicons/test.jpg",
+ "cache": "http://mediacdn.disqus.com/1304545408/images/favicon-default.png"
+ }
+ }
+}
21 spec/fixtures/forums/listCategories.json
@@ -0,0 +1,21 @@
+{
+ "cursor": {
+ "prev": null,
+ "hasNext": false,
+ "next": "595772:1:0",
+ "hasPrev": false,
+ "total": null,
+ "id": "595772:1:0",
+ "more": false
+ },
+ "code": 0,
+ "response": [
+ {
+ "id": "595772",
+ "forum": "the88",
+ "order": 0,
+ "isDefault": true,
+ "title": "General"
+ }
+ ]
+}
533 spec/fixtures/forums/listThreads.json
@@ -0,0 +1,533 @@
+{
+ "cursor": {
+ "prev": null,
+ "hasNext": true,
+ "next": "1301661689038847:0:0",
+ "hasPrev": false,
+ "total": null,
+ "id": "1301661689038847:0:0",
+ "more": true
+ },
+ "code": 0,
+ "response": [
+ {
+ "category": "595774",
+ "reactions": 2,
+ "identifiers": [
+ "post_4521"
+ ],
+ "forum": "yvanrodic",
+ "title": "Sydney",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "295818503",
+ "isClosed": false,
+ "posts": 0,
+ "link": "http://yvanrodic.com/posts/4521/sydney",
+ "likes": 0,
+ "message": "",
+ "slug": "sydney_27",
+ "createdAt": "2011-05-05T08:40:37"
+ },
+ {
+ "category": "595774",
+ "reactions": 7,
+ "identifiers": [
+ "post_4520"
+ ],
+ "forum": "yvanrodic",
+ "title": "Shanghai",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "293757871",
+ "isClosed": false,
+ "posts": 5,
+ "link": "http://yvanrodic.com/posts/4520/shanghai",
+ "likes": 0,
+ "message": "",
+ "slug": "shanghai_21",
+ "createdAt": "2011-05-03T00:35:20"
+ },
+ {
+ "category": "595774",
+ "reactions": 10,
+ "identifiers": [
+ "post_4519"
+ ],
+ "forum": "yvanrodic",
+ "title": "Shanghai",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "292599971",
+ "isClosed": false,
+ "posts": 3,
+ "link": "http://yvanrodic.com/posts/4519/shanghai",
+ "likes": 0,
+ "message": "",
+ "slug": "shanghai",
+ "createdAt": "2011-05-01T11:08:44"
+ },
+ {
+ "category": "595774",
+ "reactions": 13,
+ "identifiers": [
+ "post_4518"
+ ],
+ "forum": "yvanrodic",
+ "title": "Tokyo",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "291807157",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://yvanrodic.com/posts/4518/tokyo",
+ "likes": 0,
+ "message": "",
+ "slug": "tokyo_67",
+ "createdAt": "2011-04-30T03:38:20"
+ },
+ {
+ "category": "595774",
+ "reactions": 5,
+ "identifiers": [
+ "post_4517"
+ ],
+ "forum": "yvanrodic",
+ "title": "Seoul",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "289356160",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://yvanrodic.com/posts/4517/seoul",
+ "likes": 0,
+ "message": "",
+ "slug": "seoul_52",
+ "createdAt": "2011-04-27T04:12:03"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [
+ "post_4516"
+ ],
+ "forum": "yvanrodic",
+ "title": "Seoul",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "288449201",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://www.yvanrodic.com/posts/4516/seoul",
+ "likes": 0,
+ "message": "",
+ "slug": "seoul_08",
+ "createdAt": "2011-04-26T01:58:24"
+ },
+ {
+ "category": "595774",
+ "reactions": 16,
+ "identifiers": [
+ "post_4515"
+ ],
+ "forum": "yvanrodic",
+ "title": "Seoul",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "287593148",
+ "isClosed": false,
+ "posts": 1,
+ "link": "http://yvanrodic.com/posts/4515/seoul",
+ "likes": 0,
+ "message": "",
+ "slug": "seoul_526",
+ "createdAt": "2011-04-24T22:10:02"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [],
+ "forum": "yvanrodic",
+ "title": "Yvan Rodic - Cape Town - March 22 2011",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "286471516",
+ "isClosed": false,
+ "posts": 0,
+ "link": "http://www.yvanrodic.com/posts/4488/cape-town-march-14th-15th-2011",
+ "likes": 0,
+ "message": "",
+ "slug": "yvan_rodic_cape_town_march_22_2011",
+ "createdAt": "2011-04-23T04:52:27"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [],
+ "forum": "yvanrodic",
+ "title": "Yvan Rodic - Cape Town - March 24 2011",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "286471507",
+ "isClosed": false,
+ "posts": 0,
+ "link": "http://www.yvanrodic.com/posts/4486/cape-town-march-17th-2011",
+ "likes": 0,
+ "message": "",
+ "slug": "yvan_rodic_cape_town_march_24_2011",
+ "createdAt": "2011-04-23T04:52:25"
+ },
+ {
+ "category": "595774",
+ "reactions": 2,
+ "identifiers": [
+ "post_4514"
+ ],
+ "forum": "yvanrodic",
+ "title": "Seoul",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "284628014",
+ "isClosed": false,
+ "posts": 5,
+ "link": "http://www.yvanrodic.com/posts/4514/seoul",
+ "likes": 0,
+ "message": "",
+ "slug": "seoul_41",
+ "createdAt": "2011-04-20T21:25:23"
+ },
+ {
+ "category": "595774",
+ "reactions": 12,
+ "identifiers": [
+ "post_4513"
+ ],
+ "forum": "yvanrodic",
+ "title": "New York",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "282369868",
+ "isClosed": false,
+ "posts": 3,
+ "link": "http://yvanrodic.com/posts/4513/new-york",
+ "likes": 0,
+ "message": "",
+ "slug": "new_york_99",
+ "createdAt": "2011-04-18T12:07:42"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [
+ "post_4512"
+ ],
+ "forum": "yvanrodic",
+ "title": "New York",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "281693372",
+ "isClosed": false,
+ "posts": 3,
+ "link": "http://www.yvanrodic.com/posts/4512/new-york",
+ "likes": 0,
+ "message": "",
+ "slug": "new_york_703",
+ "createdAt": "2011-04-17T15:10:49"
+ },
+ {
+ "category": "595774",
+ "reactions": 7,
+ "identifiers": [
+ "post_4511"
+ ],
+ "forum": "yvanrodic",
+ "title": "New York",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "281253585",
+ "isClosed": false,
+ "posts": 1,
+ "link": "http://yvanrodic.com/posts/4511/new-york",
+ "likes": 0,
+ "message": "",
+ "slug": "new_york_549",
+ "createdAt": "2011-04-16T23:55:04"
+ },
+ {
+ "category": "595774",
+ "reactions": 5,
+ "identifiers": [
+ "post_4510"
+ ],
+ "forum": "yvanrodic",
+ "title": "New York",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "281227676",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://yvanrodic.com/posts/4510/new-york",
+ "likes": 0,
+ "message": "",
+ "slug": "new_york_75",
+ "createdAt": "2011-04-16T22:52:46"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [
+ "post_4509"
+ ],
+ "forum": "yvanrodic",
+ "title": "London",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "279410358",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://www.yvanrodic.com/posts/4509/london",
+ "likes": 0,
+ "message": "",
+ "slug": "london_56",
+ "createdAt": "2011-04-14T16:36:04"
+ },
+ {
+ "category": "595774",
+ "reactions": 13,
+ "identifiers": [
+ "post_4508"
+ ],
+ "forum": "yvanrodic",
+ "title": "Manchester",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "278447450",
+ "isClosed": false,
+ "posts": 11,
+ "link": "http://yvanrodic.com/posts/4508/manchester",
+ "likes": 0,
+ "message": "",
+ "slug": "manchester",
+ "createdAt": "2011-04-13T14:45:08"
+ },
+ {
+ "category": "595774",
+ "reactions": 0,
+ "identifiers": [
+ "post_4507"
+ ],
+ "forum": "yvanrodic",
+ "title": "Liverpool",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "276107815",
+ "isClosed": false,
+ "posts": 9,
+ "link": "http://www.yvanrodic.com/posts/4507/liverpool",
+ "likes": 0,
+ "message": "",
+ "slug": "liverpool",
+ "createdAt": "2011-04-10T17:31:07"
+ },
+ {
+ "category": "595774",
+ "reactions": 7,
+ "identifiers": [
+ "post_4506"
+ ],
+ "forum": "yvanrodic",
+ "title": "Milan",
+ "dislikes": 0,
+ "isDeleted": false,
+ "author": "6138058",
+ "userScore": 0,
+ "id": "275515296",
+ "isClosed": false,
+ "posts": 2,
+ "link": "http://yvanrodic.com/posts/4506/milan",
+ "likes": 0,
+ "message": "",
+ "slug": "milan_05",
+ "createdAt": "2011-04-09T18:47:49"
+ },
+ {
+ "category": "595774",
+ "reactions": 4,
+ "identifiers": [
+ "post_4505"