ruby_reddit_api-h 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ module Reddit
2
+
3
+ # @author James Cook
4
+ class Api < Base
5
+ attr_accessor :user, :password
6
+ attr_reader :last_action, :debug
7
+
8
+ def initialize(user=nil,password=nil, options={})
9
+ @user = user
10
+ @password = password
11
+ @debug = StringIO.new
12
+ end
13
+
14
+ def inspect
15
+ "<Reddit::Api>"
16
+ end
17
+
18
+ # Browse submissions by subreddit
19
+ # @param [String] Subreddit to browse
20
+ # @return [Array<Reddit::Submission>]
21
+ def browse(subreddit, options={})
22
+ subreddit = sanitize_subreddit(subreddit)
23
+ options.merge! :handler => "Submission"
24
+ if options[:limit]
25
+ options.merge!({:query => {:limit => options[:limit]}})
26
+ end
27
+ read("/r/#{subreddit}.json", options )
28
+ end
29
+
30
+ # Search reddit
31
+ # @param [String, Hash] Search terms and options
32
+ # @example
33
+ # search("programming", :in => "ruby", :sort => "relevance")
34
+ # @return [Array<Reddit::Submission>]
35
+ def search(terms=nil, options={})
36
+ http_options = {:verb => "get", :query => {}}
37
+ subreddit = options[:in]
38
+ sort = options.fetch(:sort){ "relevance" }
39
+ http_options[:query].merge!({:sort => sort})
40
+
41
+ if subreddit
42
+ http_options[:query].merge!({:restrict_sr => "1"})
43
+ end
44
+
45
+ if terms
46
+ http_options[:query].merge!({:q => terms})
47
+ end
48
+ path = subreddit.to_s == "" ? "/search.json" : "/r/#{subreddit}/search.json"
49
+ read(path, http_options)
50
+ end
51
+
52
+ # Read sent messages
53
+ # @return [Array<Reddit::Message>]
54
+ def sent_messages
55
+ messages :sent
56
+ end
57
+
58
+ # Read received messages
59
+ # @return [Array<Reddit::Message>]
60
+ def received_messages
61
+ messages :inbox
62
+ end
63
+
64
+ #Read unread messages
65
+ # @return [Array<Reddit::Message>]
66
+ def unread_messages
67
+ messages :unread
68
+ end
69
+
70
+ # Read received comments
71
+ # @return [Array<Reddit::Message>]
72
+ def comments
73
+ messages :comments
74
+ end
75
+
76
+ # Read post replies
77
+ # @return [Array<Reddit::Message>]
78
+ def post_replies
79
+ messages :selfreply
80
+ end
81
+
82
+ protected
83
+ def messages(kind)
84
+ read("/message/#{kind.to_s}.json", :handler => "Message")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,133 @@
1
+ module Reddit
2
+ # Base module all other classes descend from. Stores the cookie, modhash and user info for many API calls. Handles authentication and directs JSON
3
+ # to the relevant handler.
4
+ # @author James Cook
5
+ class Base
6
+ include HTTParty
7
+
8
+ attr_reader :last_action, :debug
9
+ base_uri "www.reddit.com"
10
+ class << self; attr_reader :cookie, :modhash, :user_id, :user, :throttle_duration end
11
+
12
+ @throttle_duration = 1.0
13
+
14
+ def initialize(options={})
15
+ @debug = StringIO.new
16
+ end
17
+
18
+ # @return [String]
19
+ def inspect
20
+ "<Reddit::Base>"
21
+ end
22
+
23
+ # Login to Reddit and capture the cookie
24
+ # @return [Boolean] Login success or failure
25
+ def login
26
+ capture_session(self.class.post( "/api/login", {:body => {:user => @user, :passwd => @password}, :debug_output => @debug} ) )
27
+ logged_in?
28
+ end
29
+
30
+ # Remove the cookie to effectively logout of Reddit
31
+ # @return [nil]
32
+ def logout
33
+ Reddit::Base.instance_variable_set("@cookie",nil)
34
+ end
35
+
36
+ # @return [String, nil]
37
+ def cookie
38
+ Reddit::Base.cookie
39
+ end
40
+
41
+ # A kind of authenticity token for many API calls
42
+ # @return [String, nil]
43
+ def modhash
44
+ Reddit::Base.modhash
45
+ end
46
+
47
+ # Reddit's displayed ID for the logged in user
48
+ # @return [String]
49
+ def user_id
50
+ Reddit::Base.user_id
51
+ end
52
+
53
+ # Logged in user
54
+ # @return [String]
55
+ def user
56
+ Reddit::Base.user
57
+ end
58
+
59
+ # The session is authenticated if the captured cookie shows a valid session is in place
60
+ # @return [true,false]
61
+ def logged_in?
62
+ !!(cookie && (cookie =~ /reddit_session/) != nil)
63
+ end
64
+
65
+ # @return String
66
+ def user_agent
67
+ self.class.user_agent
68
+ end
69
+
70
+ # HTTP headers to be sent in all API requests. At a minimum, 'User-agent' and 'Cookie' are needed.
71
+ # @return Hash
72
+ def base_headers
73
+ self.class.base_headers
74
+ end
75
+
76
+ # Base communication function for nearly all API calls
77
+ # @return [Reddit::Submission, Reddit::Comment, Reddit::User, false] Various reddit-related, json parseable objects
78
+ def read(url, options={})
79
+ unless throttled?
80
+ @debug.rewind
81
+ verb = (options[:verb] || "get")
82
+ param_key = (verb == "get") ? :query : :body
83
+ resp = self.class.send( verb, url, {param_key => (options[param_key] || {}), :headers => base_headers, :debug_output => @debug})
84
+ if valid_response?(resp)
85
+ @last_action = Time.now
86
+ klass = Reddit.const_get(options[:handler] || "Submission")
87
+ resp = klass.parse( JSON.parse(resp.body, :max_nesting => 9_999) )
88
+ return resp
89
+ else
90
+ return false
91
+ end
92
+ end
93
+ end
94
+
95
+ protected
96
+ def valid_response?(response)
97
+ response.code == 200 && response.headers["content-type"].to_s =~ /json/
98
+ end
99
+
100
+ def capture_session(response)
101
+ cookies = response.headers["set-cookie"]
102
+ Reddit::Base.instance_variable_set("@cookie", cookies)
103
+ Reddit::Base.instance_variable_set("@user", @user)
104
+ end
105
+
106
+ def capture_user_id
107
+ return true if user_id
108
+ this_user = read("/user/#{user}/about.json", :handler => "User")[0]
109
+ Reddit::Base.instance_variable_set("@user_id", this_user.id)
110
+ end
111
+
112
+ def throttled?
113
+ @last_action && ( ( Time.now - @last_action ) < Reddit::Base.throttle_duration )
114
+ end
115
+
116
+ def sanitize_subreddit(subreddit)
117
+ subreddit.gsub!(/^\/?r\//,'')
118
+ subreddit.gsub!(/\.json\Z/,'')
119
+ subreddit
120
+ end
121
+
122
+ class << self
123
+
124
+ def base_headers
125
+ {'Cookie' => Reddit::Base.cookie.to_s, 'user-agent' => user_agent}
126
+ end
127
+
128
+ def user_agent
129
+ "Ruby Reddit Client v#{Reddit::VERSION}"
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,125 @@
1
+ module Reddit
2
+ # @author James Cook
3
+ class Comment < Thing
4
+ include Voteable
5
+ include JsonListing
6
+
7
+ attr_reader :body, :subreddit_id, :name, :created, :downs, :author, :created_utc, :body_html, :link_id, :parent_id, :likes, :num_comments, :subreddit, :ups, :debug, :kind, :replies
8
+ def initialize(json, options={})
9
+ mode = options.fetch(:mode){ :json }
10
+ if mode == :json
11
+ parse(json)
12
+ end
13
+
14
+ if mode == :replies
15
+ json.keys.each do |key|
16
+ instance_variable_set("@#{key}", json[key])
17
+ end
18
+ end
19
+
20
+ if replies.is_a?(Hash)
21
+ handle_replies(replies)
22
+ end
23
+
24
+ @debug = StringIO.new
25
+ end
26
+
27
+ def inspect
28
+ "<Reddit::Comment author='#{@author}' body='#{short_body}' replies='#{replies.size}' ups='#{ups}' downs='#{downs}'>"
29
+ end
30
+
31
+ # @return [String]
32
+ def to_s
33
+ body
34
+ end
35
+
36
+ # Fetch comments
37
+ # @return [Array<Reddit::Comment>]
38
+ def comments
39
+ opts = {:handler => "Comment",
40
+ :verb => "post",
41
+ :body =>
42
+ {:link_id => id, :depth => 10, :r => subreddit, :uh => modhash, :renderstyle => "json", :pv_hex => "", :id => id}
43
+ }
44
+ return read("/api/morechildren", opts )
45
+ end
46
+
47
+ # Modify a comment
48
+ # @return [true,false]
49
+ def edit(newtext)
50
+ resp=self.class.post("/api/editusertext", {:body => {:thing_id => id, :uh => modhash, :r => subreddit, :text => newtext }, :headers => base_headers, :debug_output => @debug })
51
+ resp.code == 200
52
+ end
53
+
54
+ # Hide a comment
55
+ # @return [true,false]
56
+ def hide
57
+ resp=self.class.post("/api/del", {:body => {:id => id, :uh => modhash, :r => subreddit, :executed => "removed" }, :headers => base_headers, :debug_output => @debug })
58
+ resp.code == 200
59
+ end
60
+
61
+ # Remove a comment
62
+ # @return [true,false]
63
+ def remove
64
+ resp=self.class.post("/api/remove", {:body => {:id => id, :uh => modhash, :r => subreddit}, :headers => base_headers, :debug_output => @debug })
65
+ resp.code == 200
66
+ end
67
+
68
+ # Approve a comment
69
+ # @return [true,false]
70
+ def approve
71
+ resp=self.class.post("/api/approve", {:body => {:id => id, :uh => modhash, :r => subreddit}, :headers => base_headers, :debug_output => @debug })
72
+ resp.code == 200
73
+ end
74
+
75
+ def moderator_distinguish
76
+ add_distinction "yes"
77
+ end
78
+
79
+ def admin_distinguish
80
+ add_distinction "admin"
81
+ end
82
+
83
+ def indistinguish
84
+ add_distinction "no"
85
+ end
86
+
87
+ # Reply to another comment
88
+ # @return [true,false]
89
+ def reply(text)
90
+ resp = self.class.post("/api/comment", {:body => {:thing_id => id, :text => text, :uh => modhash, :r => subreddit }, :headers => base_headers, :debug_output => @debug })
91
+ resp.code == 200
92
+ end
93
+
94
+ # Trimmed comment body suitable for #inspect
95
+ # @return [String]
96
+ def short_body
97
+ str = body.to_s.strip
98
+ if str.size > 15
99
+ sb = str[0..15] + "..."
100
+ else
101
+ sb = body
102
+ end
103
+
104
+ sb.gsub(/[\n\r]/,'\n')
105
+ end
106
+
107
+ def add_distinction(verb)
108
+ resp=self.class.post("/api/distinguish/#{verb}", {:body => {:id => id, :uh => modhash, :r => subreddit, :executed => "distinguishing..."}, :headers => base_headers, :debug_output => @debug })
109
+ resp.code == 200
110
+ end
111
+
112
+ # Parse nested comment hashes into a Comment
113
+ # @param [Hash] JSON containing reply data
114
+ # @return [true]
115
+ def handle_replies(data)
116
+ dup = data.dup
117
+ _kind, data = dup["kind"], dup["data"]
118
+ modhash, children = data["modhash"], data["children"]
119
+ if children.is_a?(Array)
120
+ @replies = children.map{|reply| Comment.new(reply["data"], :mode => :replies) }
121
+ end
122
+ true
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,46 @@
1
+ #@author James Cook
2
+ module Reddit
3
+ module JsonListing
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.send(:include, InstanceMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # @param [Hash] JSON received from Reddit
12
+ # @return [Array<Reddit::Submission, Reddit::User, Reddit::Comment, Reddit::Message>]
13
+ def parse(json)
14
+ results = []
15
+ if json.is_a?(Array)
16
+ json.each do |j|
17
+ results << parse(j)
18
+ end
19
+ else
20
+ data = json["data"]
21
+ Reddit::Base.instance_variable_set("@modhash", data["modhash"]) # Needed for api calls
22
+
23
+ children = data["children"] || [{"data" => data, "kind" => json["kind"] }]
24
+ children.each do |message|
25
+ kind = message["kind"]
26
+ message["data"]["kind"] = kind
27
+ results << self.new(message["data"])
28
+ end
29
+ end
30
+
31
+ return results.flatten
32
+ end
33
+ end
34
+ module InstanceMethods
35
+ # Iterate over JSON and set instance variables that map to each JSON key
36
+ # @param [Hash] JSON received from Reddit
37
+ # @return [true]
38
+ def parse(json)
39
+ json.keys.each do |key|
40
+ instance_variable_set("@#{key}", json[key])
41
+ end
42
+ true
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ # @author James Cook
2
+ module Reddit
3
+ class Message < Thing
4
+ include JsonListing
5
+
6
+ attr_reader :body, :was_comment, :kind, :first_message, :name, :created, :dest, :created_utc, :body_html, :subreddit, :parent_id, :context, :replies, :subject, :debug
7
+ def initialize(json)
8
+ parse(json)
9
+ @debug = StringIO.new
10
+ end
11
+
12
+ # The reddit ID of this message
13
+ # @return [String]
14
+ def id
15
+ "#{kind}_#{@id}"
16
+ end
17
+
18
+ def unread?
19
+ @new == true
20
+ end
21
+
22
+ def read?
23
+ @new == false
24
+ end
25
+
26
+ # The author of the message. The data is lazy-loaded and cached on the message
27
+ # @return [Reddit::User]
28
+ def author
29
+ @author_data ||= read("/user/#{@author}/about.json", :handler => "User")
30
+ end
31
+
32
+ # Reply to this message
33
+ # @return [true,false]
34
+ def reply(text)
35
+ resp = self.class.post("/api/comment", {:body => {:thing_id => id, :text => text, :uh => modhash, :r => subreddit }, :headers => base_headers, :debug_output => @debug })
36
+ resp.code == 200
37
+ end
38
+
39
+ def mark_unread
40
+ resp = self.class.post("/api/unread_message", {:body => {:id => id, :uh => modhash}, :headers => base_headers, :debug_output => @debug })
41
+ resp.code == 200
42
+ end
43
+
44
+ def mark_read
45
+ resp = self.class.post("/api/read_message", {:body => {:id => id, :uh => modhash }, :headers => base_headers, :debug_output => @debug })
46
+ resp.code == 200
47
+ end
48
+
49
+ def inspect
50
+ "<Reddit::Message '#{short_body}'>"
51
+ end
52
+
53
+ # Trimmed comment body suitable for #inspect
54
+ # @return [String]
55
+ def short_body
56
+ if body.size > 15
57
+ body[0..15]
58
+ else
59
+ body
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,39 @@
1
+ # @author James Cook
2
+ module Reddit
3
+ class MessageGroup < Thing
4
+ include JsonListing
5
+ attr_reader :debug
6
+
7
+ def initialize
8
+ @debug = StringIO.new
9
+ end
10
+
11
+ def mark_read(messages)
12
+ mark messages, "read"
13
+ end
14
+
15
+ def mark_unread(messages)
16
+ mark messages, "unread"
17
+ end
18
+
19
+ protected
20
+ def mark(messages, action)
21
+ ids = ids(messages)
22
+ action = action == "read" ? "read_message" : "unread_message"
23
+ resp = self.class.post("/api/#{action}", {:body => {:id => ids.join(','), :uh => modhash }, :headers => base_headers, :debug_output => @debug })
24
+ resp.code == 200
25
+ end
26
+
27
+ def ids(messages)
28
+ if messages.is_a?(Array)
29
+ if messages.first.is_a?(Reddit::Message)
30
+ return messages.map{|m| m.id }
31
+ else
32
+ return messages # assume array of String
33
+ end
34
+ else
35
+ return [ messages.id ]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ # @author James Cook
2
+ module Reddit
3
+ class Submission < Thing
4
+ include JsonListing
5
+ include Voteable
6
+
7
+ attr_reader :domain, :media_embed, :subreddit, :selftext_html, :selftext, :likes, :saved, :clicked, :media, :score, :over_18, :hidden, :thumbnail, :subreddit_id, :downs, :is_self, :permalink, :name, :created, :url, :title, :created_utc, :num_comments, :ups, :kind, :last_comment_id
8
+
9
+ def inspect
10
+ "<Reddit::Submission id='#{id}' author='#{@author}' title='#{title}'>"
11
+ end
12
+
13
+ # Add a comment to a submission
14
+ # @param [String] Comment text
15
+ # @return [true,false]
16
+ def add_comment(text)
17
+ resp = self.class.post("/api/comment", {:body => {:thing_id => id, :text => text, :uh => modhash, :r => subreddit }, :headers => base_headers, :debug_output => @debug })
18
+ resp.code == 200
19
+ end
20
+
21
+ # Save submission
22
+ # @return [true,false]
23
+ def save
24
+ toggle :save
25
+ end
26
+
27
+ # Unsave submission
28
+ # @return [true,false]
29
+ def unsave
30
+ toggle :unsave
31
+ end
32
+
33
+ # Hide submission
34
+ # @return [true,false]
35
+ def hide
36
+ toggle :hide
37
+ end
38
+
39
+ # Unhide submission
40
+ # @return [true,false]
41
+ def unhide
42
+ toggle :unhide
43
+ end
44
+
45
+ def moderator_distinguish
46
+ add_distinction "yes"
47
+ end
48
+
49
+ def admin_distinguish
50
+ add_distinction "admin"
51
+ end
52
+
53
+ def indistinguish
54
+ add_distinction "no"
55
+ end
56
+
57
+ # Fetch submission comments
58
+ # @todo Move to 'Thing' class
59
+ # @return [Array<Reddit::Comment>]
60
+ def comments
61
+ _comments = read( permalink + ".json", {:handler => "Comment", :query => {:limit => 250}} )
62
+ @last_comment_id = _comments.last.id if _comments && _comments.any?
63
+ _comments.shift # First 'comment' is actually the submission
64
+ return _comments
65
+ end
66
+
67
+ protected
68
+ def add_distinction(verb)
69
+ resp=self.class.post("/api/distinguish/#{verb}", {:body => {:id => id, :uh => modhash, :r => subreddit, :executed => "distinguishing..."}, :headers => base_headers, :debug_output => @debug })
70
+ resp.code == 200
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,38 @@
1
+ module Reddit
2
+ # @author James Cook
3
+ class Thing < Base
4
+ include JsonListing
5
+
6
+ def initialize(data)
7
+ parse(data)
8
+ @debug = StringIO.new
9
+ end
10
+
11
+ # The reddit ID of this entity
12
+ # @return [String]
13
+ def id
14
+ "#{kind}_#{@id}"
15
+ end
16
+
17
+ # The author of the entity. The data is lazy-loaded and cached on the object
18
+ # @return [Reddit::User]
19
+ def author
20
+ response = read("/user/#{@author}/about.json", :handler => "User") if @author
21
+ @author_data ||= response[0] if response
22
+ end
23
+
24
+ # Report thing
25
+ # @return [true,false]
26
+ def report
27
+ toggle :report
28
+ end
29
+
30
+ def toggle(which)
31
+ return false unless logged_in?
32
+ mapping = {:save => "save", :unsave => "unsave", :hide => "hidden", :unhide => "unhidden", :report => "report"}
33
+ mode = mapping[which]
34
+ resp = self.class.post("/api/#{which}", {:body => {:id => id, :uh => modhash, :r => subreddit, :executed => mode }, :headers => base_headers, :debug_output => @debug })
35
+ resp.code == 200
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ module Reddit
2
+ # @author James Cook
3
+ class User < Api
4
+ include JsonListing
5
+ attr_reader :name, :debug, :created, :created_utc, :link_karma, :comment_karma, :is_mod, :has_mod_mail, :kind
6
+ def initialize(json)
7
+ @debug = StringIO.new
8
+ parse(json)
9
+ end
10
+
11
+ def inspect
12
+ "<Reddit::User name='#{name}'>"
13
+ end
14
+
15
+ # The reddit ID of this submission
16
+ # @return [String]
17
+ def id
18
+ "#{kind}_#{@id}"
19
+ end
20
+
21
+ # @return [String]
22
+ def to_s
23
+ name
24
+ end
25
+
26
+ # Add redditor as friend. Requires a authenticated user.
27
+ # @return [true,false]
28
+ def friend
29
+ capture_user_id
30
+ resp=self.class.post("/api/friend", {:body => {:name => name, :container => user_id, :type => "friend", :uh => modhash}, :headers => base_headers, :debug_output => @debug })
31
+ resp.code == 200
32
+ end
33
+
34
+ # Remove redditor as friend. Requires a authenticated user.
35
+ # @return [true,false]
36
+ def unfriend
37
+ capture_user_id
38
+ resp=self.class.post("/api/unfriend", {:body => {:name => name, :container => user_id, :type => "friend", :uh => modhash}, :headers => base_headers, :debug_output => @debug })
39
+ resp.code == 200
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ # @author James Cook
2
+ module Reddit
3
+ # Capture the code version from the VERSION file
4
+ # @return [String]
5
+ VERSION = File.exist?("VERSION") ? File.read("VERSION").chomp : ""
6
+ end
@@ -0,0 +1,39 @@
1
+ # @author James Cook
2
+ module Reddit
3
+ class Vote < Base
4
+ attr_reader :submission
5
+
6
+ def initialize(submission)
7
+ @submission = submission
8
+ end
9
+
10
+ # Upvote submission or comment
11
+ # @return [true,false]
12
+ def up
13
+ vote(:up)
14
+ end
15
+
16
+ # Downvote submission or comment
17
+ # @return [true,false]
18
+ def down
19
+ vote(:down)
20
+ end
21
+
22
+ #@return [String]
23
+ def inspect
24
+ "<Reddit::Vote>"
25
+ end
26
+
27
+ protected
28
+ def vote(direction)
29
+ return false unless logged_in?
30
+ up_or_down = direction == :up ? 1 : -1
31
+ resp = self.class.post( "/api/vote", {:body => {:id => submission.id, :dir => up_or_down, :uh => modhash}, :headers => base_headers})
32
+ if resp.code == 200
33
+ return true
34
+ else
35
+ return false
36
+ end
37
+ end
38
+ end
39
+ end