reddit-api 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "reddit/api"
@@ -0,0 +1,14 @@
1
+ require "reddit/api/version"
2
+ # Require External Libs
3
+ require "json"
4
+ require "pry"
5
+ require "rest-client"
6
+ require "log4r"
7
+
8
+ # Require Internal Files
9
+ require "reddit/internal/logger"
10
+ require "reddit/internal/processor"
11
+ require "reddit/internal/connection"
12
+
13
+ require "reddit/services"
14
+ require "reddit/services/user"
@@ -0,0 +1,5 @@
1
+ module Reddit
2
+ module Api
3
+ VERSION = "0.1.1"
4
+ end
5
+ end
@@ -0,0 +1,143 @@
1
+ module Reddit
2
+ module Internal
3
+ class Connection
4
+
5
+ # User Information
6
+ attr_accessor :username
7
+ attr_accessor :password
8
+ attr_accessor :client_id
9
+ attr_accessor :secret
10
+ attr_accessor :user_agent_title
11
+
12
+ # Token Info
13
+ attr_accessor :token
14
+ attr_accessor :token_expiration
15
+
16
+ # Rate Limiting Info
17
+ attr_accessor :request_throttle
18
+ attr_accessor :requests_per_minute
19
+ attr_reader :last_request_time
20
+
21
+ # Retry Info
22
+ attr_accessor :max_retries
23
+
24
+ # Creates a new connection
25
+ def initialize(username, password, client_id, secret, user_agent_title, request_throttle: nil, max_retries: nil)
26
+
27
+ # Set up the module
28
+ @username = username
29
+ @password = password
30
+ @client_id = client_id
31
+ @secret = secret
32
+ @user_agent_title = user_agent_title
33
+ @token = nil
34
+
35
+ @request_throttle = request_throttle == nil ? true : false
36
+ @requests_per_minute = 60
37
+ @last_request_time = Time.now.to_f
38
+
39
+ @max_retries = max_retries ||= 3
40
+
41
+ at_exit do
42
+ self.sign_out()
43
+ end
44
+ end
45
+
46
+
47
+ # Signs In A User, Making The Connection Active
48
+ def sign_in()
49
+
50
+ Reddit::Internal::Logger.log.debug "Signing In User #{@username} with client_id #{@client_id}..."
51
+ response = JSON.parse(RestClient::Request.execute(method: :post,
52
+ url: "https://www.reddit.com/#{Reddit::Services::REFERENCE["Auth"]["access_token"]["url"]}",
53
+ payload: "grant_type=password&username=#{@username}&password=#{@password}",
54
+ user: @client_id, password: @secret))
55
+
56
+
57
+ Reddit::Internal::Logger.log.debug "Sign In Response:"
58
+ Reddit::Internal::Logger.log.debug JSON.pretty_generate(response)
59
+
60
+ raise "Error Invalid Credentials" if response.include?("error")
61
+
62
+ @token_expiration = Time.now.to_f + response["expires_in"]
63
+ @token = response["access_token"]
64
+
65
+ Reddit::Internal::Logger.log.info "Sign In Retrieved Token: #{@token}"
66
+ return @token
67
+ end
68
+
69
+
70
+ # Signs Out A User, Killing the connection
71
+ def sign_out()
72
+ if @token
73
+ Reddit::Internal::Logger.log.debug "Signing Out User #{@username} with client_id #{@client_id}..."
74
+ response = RestClient::Request.execute(method: :post,
75
+ url: "https://www.reddit.com/#{Reddit::Services::REFERENCE["Auth"]["revoke_token"]["url"]}",
76
+ payload: "token=#{@token}",
77
+ user: @client_id, password: @secret)
78
+
79
+ Reddit::Internal::Logger.log.debug "Sign Out Response: #{response}"
80
+
81
+ Reddit::Internal::Logger.log.info "Signed Out User #{@username}"
82
+ @token = nil
83
+ else
84
+
85
+ end
86
+ end
87
+
88
+ # Refreshes a token (BROKEN? 400 bad request, possibly due to token still being valid...)
89
+ def refresh()
90
+ Reddit::Internal::Logger.log.debug "Refreshing Token For User #{@username} with client_id #{@client_id}"
91
+ response = JSON.parse(RestClient::Request.execute(method: :post,
92
+ url: "https://www.reddit.com/#{Reddit::Services::REFERENCE["Auth"]["access_token"]["url"]}",
93
+ payload: "grant_type=refresh_token&refresh_token=#{@token}",
94
+ user: @client_id, password: @secret))
95
+
96
+ Reddit::Internal::Logger.log.debug "Refresh Response:"
97
+ Reddit::Internal::Logger.log.debug JSON.pretty_generate(response)
98
+
99
+ return response
100
+ end
101
+
102
+
103
+ # Handles Requests From This Users Auth
104
+ def request(method, url, payload)
105
+
106
+ raise "User Not Signed In!" unless @token
107
+
108
+ refresh() if @token_expiration < Time.now.to_f
109
+
110
+ Reddit::Internal::Logger.log.debug "Reqest From User #{@username}, method: #{method}, url: #{url}"
111
+
112
+ if @request_throttle
113
+ Reddit::Internal::Logger.log.debug "Request Throttling Enabled, Processing Request at #{Time.now.to_f}, Last Call: #{@last_request_time}..."
114
+ request_wait_time = @requests_per_minute / 60
115
+ next_avaliable = @last_request_time + request_wait_time
116
+ if Time.now.to_f < next_avaliable
117
+ sleep_time = (next_avaliable - Time.now.to_f)
118
+ Reddit::Internal::Logger.log.info "Rate Limiter Sleeping For #{sleep_time}"
119
+ sleep sleep_time
120
+ end
121
+ end
122
+
123
+ # Handle getting a new token if we expired...
124
+
125
+ retries = @max_retries
126
+ begin
127
+ response = JSON.parse(RestClient::Request.execute(method: method, url: url, payload: payload,
128
+ headers: {"Authorization" => "bearer #{token}", "User-Agent" => "ruby-reddit-api:#{user_agent_title}"} ))
129
+ rescue StandardError => e
130
+ retry unless (retries -= 1).zero?
131
+ raise e
132
+ end
133
+ Reddit::Internal::Logger.log.debug "Request Response:"
134
+ Reddit::Internal::Logger.log.debug JSON.pretty_generate(response)
135
+
136
+ @last_request_time = Time.now.to_f
137
+ return response
138
+ end
139
+
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,17 @@
1
+ module Reddit
2
+ module Internal
3
+ module Logger
4
+ extend self
5
+
6
+ attr_accessor :log
7
+ attr_accessor :enable_file_logging
8
+ @enable_file_logging = false
9
+
10
+ @log = Log4r::Logger.new("reddit_api.log")
11
+ @log.outputters << Log4r::FileOutputter.new("reddit_api.log", filename: "reddit_api.log") if @enable_file_logging
12
+ @log.outputters << Log4r::Outputter.stdout
13
+
14
+ @log.level = Log4r::WARN
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ module Reddit
2
+ module Internal
3
+ module Processor
4
+ extend self
5
+
6
+ attr_accessor :base_url
7
+ @base_url = "https://oauth.reddit.com/"
8
+
9
+
10
+ def process(module_name, function_name, user, payload, var_hash)
11
+ ref_data = Reddit::Services::REFERENCE[module_name][function_name.split("_").drop(1).join("/")]
12
+
13
+
14
+ # URL Building
15
+ if ref_data["fields"].include?("basepath_subreddit")
16
+ raise "Basepath Subreddit Required But Not Passed!" unless var_hash[:basepath_subreddit]
17
+ # Subreddit Exception (As its not listed in the json file)
18
+ url = "#{@base_url}r/#{var_hash[:basepath_subreddit]}/#{ref_data["url"]}"
19
+ else
20
+ url = "#{@base_url}#{ref_data["url"]}"
21
+ end
22
+
23
+
24
+ # Url Basepath
25
+ var_hash.each do |k,v|
26
+ if k.to_s.include?("basepath_") && !k.to_s.include?("basepath_subreddit")
27
+ url.gsub!(k.to_s.split("_").drop(1).join("_"), v)
28
+ end
29
+ end
30
+
31
+ # Url Params
32
+ url += "?" + var_hash.collect { |k,v| "#{k.to_s}=#{v.to_s}" unless k.to_s.include?("basepath_") }.join("&") if var_hash.length > var_hash.select{|k,v| k.to_s.include?("basepath_")}.length
33
+
34
+
35
+ # Make the request via the user
36
+ user.connection.request(ref_data["method"], url, payload)
37
+ end
38
+
39
+
40
+ def batch_call(module_name, function_name, user, var_hash)
41
+ #Setup Local Vars
42
+ page_size = var_hash[:page_size]
43
+ max_size = var_hash[:max_size]
44
+ remove_sticky = var_hash[:remove_sticky]
45
+
46
+ remove_sticky = true if remove_sticky == nil
47
+
48
+ raise "page_size parameter missing!" unless page_size
49
+ raise "max_size parameter missing!" unless max_size
50
+ raise "page_size cannot be zero!" if page_size == 0
51
+
52
+ # Delete Paging Specific Hash Set
53
+ var_hash.delete(:page_size)
54
+ var_hash.delete(:max_size)
55
+ var_hash.delete(:remove_sticky)
56
+
57
+ # Set up our out of loop variables
58
+ total_results = []
59
+ last_entry = nil
60
+
61
+ # Process Requests
62
+ Reddit::Internal::Logger.log.debug "Processing Batch #{module_name} -> #{function_name}:"
63
+ (max_size / page_size).times do |index|
64
+ Reddit::Internal::Logger.log.debug "Batch [#{index} / #{(max_size / page_size)}]"
65
+ # Setup Vars For Local Batch
66
+ page_var_hash = var_hash ? var_hash : {}
67
+ page_var_hash["after"] = last_entry if last_entry
68
+ page_var_hash["count"] = index * page_size
69
+ page_var_hash["limit"] = page_size
70
+
71
+ # Fetch Batch
72
+ result_batch = Reddit::Internal::Processor.process(module_name, function_name, user, nil, page_var_hash)
73
+ result_batch = result_batch["data"]["children"]
74
+
75
+ # Remove Extra Sticky Posts
76
+ result_batch = result_batch.drop(result_batch.length - page_size) unless last_entry if remove_sticky
77
+
78
+ # Merge Results
79
+ total_results += result_batch
80
+
81
+ # Check if break / setup for next set.
82
+ break if result_batch.length < page_size
83
+ last_entry = result_batch.last["data"]["name"]
84
+ end
85
+ return total_results
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,54 @@
1
+ module Reddit
2
+ module Services
3
+ extend self
4
+ Reddit::Internal::Logger.log.debug "Loading REFERENCE."
5
+ REFERENCE = JSON.parse(File.read(File.expand_path("../../../data/reddit_api.json", __FILE__)))
6
+ Reddit::Internal::Logger.log.debug "REFERENCE Loaded."
7
+
8
+ private
9
+ # Generates a given function
10
+ def generate_function(new_module, module_name, hash)
11
+ # Generate our Get and Post Functions
12
+ method_name = "#{hash["method"]}_#{hash["name"].split("/").join("_")}"
13
+
14
+ Reddit::Internal::Logger.log.debug "Generating Funcgion #{module_name}.#{method_name}"
15
+
16
+ if hash["method"] == "post"
17
+ new_module.define_singleton_method(method_name) do |user, payload, options = {}|
18
+ Reddit::Internal::Processor.process(module_name, method_name, user, payload, options)
19
+ end
20
+ else
21
+ new_module.define_singleton_method(method_name) do |user, options = {}|
22
+ Reddit::Internal::Processor.process(module_name, method_name, user, nil, options)
23
+ end
24
+ end
25
+
26
+ #Generate Print Function
27
+ print_name = "print_#{method_name}"
28
+ new_module.define_singleton_method(print_name) do
29
+ Reddit::Internal::Logger.debug "Fields: #{REFERENCE[module_name][hash["name"]]["fields"]}"
30
+ REFERENCE[module_name][hash["name"]]["fields"]
31
+ end
32
+
33
+ # Generate the batch functions on GET endpoints for listings (As other endpoints tend to output different data formats)
34
+ if hash["method"] == "get" && module_name == :Listings
35
+ batch_name = "batch_#{hash["name"].split("/").join("_")}"
36
+ new_module.define_singleton_method(batch_name) do |user, options = {}|
37
+ Reddit::Internal::Processor.batch_call(module_name, method_name, user, options)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Itterate through the given JSON and generate modules and functions
43
+ REFERENCE.each do |api_module, module_functions|
44
+ Reddit::Internal::Logger.log.debug "Generating Module: #{api_module}"
45
+ new_module = Module.new do
46
+ module_functions.each do |name, function_hash|
47
+ Reddit::Services.send(:generate_function, self, api_module, function_hash)
48
+ end
49
+ end
50
+ Reddit::Services.const_set(api_module, new_module)
51
+ Reddit::Internal::Logger.log.debug "Generating Module: #{api_module} Completed."
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,23 @@
1
+ module Reddit
2
+ module Services
3
+ # Everything else in the services module is created at runtime through the "service.rb" generator
4
+
5
+ class User
6
+
7
+ attr_accessor :name
8
+ attr_accessor :token
9
+
10
+ attr_accessor :connection
11
+
12
+ def initialize(username, password, client_id, secret, user_agent_title, options = {})
13
+
14
+ raise "user_agent_title must be set, please follow the reddit API rules" if user_agent_title == nil
15
+
16
+ @name = username
17
+ @connection = Reddit::Internal::Connection.new(username, password, client_id, secret, user_agent_title, options)
18
+ @connection.sign_in()
19
+ @token = @connection.token
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ MIT License:
2
+
3
+ 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:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ 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.
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'reddit/api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "reddit-api"
8
+ spec.version = Reddit::Api::VERSION
9
+ spec.authors = ["="]
10
+ spec.email = ["="]
11
+
12
+ spec.summary = "A interface to the reddit API. A clean and simple aproach. Get prying!"
13
+ spec.description = ""
14
+ spec.homepage = "https://github.com/karl-b/reddit-api"
15
+
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+
26
+ spec.add_runtime_dependency "rest-client", "~> 1.0"
27
+ spec.add_runtime_dependency "log4r"
28
+ spec.add_runtime_dependency "pry"
29
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reddit-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - '='
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-02-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rest-client
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: log4r
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: ''
98
+ email:
99
+ - '='
100
+ executables:
101
+ - reddit-api
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - data.json
114
+ - data/reddit_api.json
115
+ - exe/reddit-api
116
+ - lib/reddit/api.rb
117
+ - lib/reddit/api/version.rb
118
+ - lib/reddit/internal/connection.rb
119
+ - lib/reddit/internal/logger.rb
120
+ - lib/reddit/internal/processor.rb
121
+ - lib/reddit/services.rb
122
+ - lib/reddit/services/user.rb
123
+ - license.txt
124
+ - reddit-api.gemspec
125
+ homepage: https://github.com/karl-b/reddit-api
126
+ licenses: []
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.5.1
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: A interface to the reddit API. A clean and simple aproach. Get prying!
148
+ test_files: []