snooby 0.0.1 → 0.1.0
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.
- data/Gemfile +1 -3
- data/README.md +42 -0
- data/Rakefile +1 -1
- data/lib/snooby.rb +75 -9
- data/lib/snooby/actions.rb +69 -0
- data/lib/snooby/client.rb +65 -0
- data/lib/snooby/objects.rb +37 -0
- metadata +33 -10
- data/.gitignore +0 -4
- data/lib/snooby/version.rb +0 -3
- data/snooby.gemspec +0 -20
data/Gemfile
CHANGED
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# What is Snooby?
|
2
|
+
Snooby is a wrapper around the reddit API written in Ruby. It aims to make automating any part of the reddit experience as simple and clear-cut as possible, while still providing ample functionality.
|
3
|
+
|
4
|
+
## Install
|
5
|
+
gem install snooby
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
Here's one way you might go about implementing a very simple bot that constantly monitors new comments to scold users of crass language.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'snooby'
|
13
|
+
|
14
|
+
probot = Snooby::Client.new('ProfanityBot, v1.0')
|
15
|
+
probot.authorize!('ProfanityBot', 'hunter2')
|
16
|
+
while true
|
17
|
+
new_comments = probot.r('all').comments
|
18
|
+
sleep 2 # Respecting the API is currently manual, will be fixed in future.
|
19
|
+
new_comments.each do |com|
|
20
|
+
if com.body =~ /(vile|rotten|words)/
|
21
|
+
com.reply("#{$&.capitalize} is a terrible word, #{com.author}!")
|
22
|
+
sleep 2
|
23
|
+
end
|
24
|
+
end
|
25
|
+
sleep 2
|
26
|
+
end
|
27
|
+
```
|
28
|
+
## Features
|
29
|
+
|
30
|
+
Snooby is in the early stages of active development. Most of the code is structure, but there is *some* functionality in place. At the moment, Snooby can:
|
31
|
+
|
32
|
+
* grab the first page of comments/posts for a user/subreddit
|
33
|
+
* grab about data for users and subreddits
|
34
|
+
* grab trophy data
|
35
|
+
* reply to comments and posts
|
36
|
+
|
37
|
+
## TODO
|
38
|
+
|
39
|
+
* Pagination
|
40
|
+
* Flesh out errors
|
41
|
+
* Much more thorough configuration file
|
42
|
+
* Granular caching
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require "bundler/gem_tasks"
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/snooby.rb
CHANGED
@@ -1,14 +1,80 @@
|
|
1
|
-
|
2
|
-
%w[net/http cgi json].each { |d| require d }
|
3
|
-
require 'snooby/user'
|
1
|
+
%w[net/http/persistent json].each { |d| require d }
|
4
2
|
|
5
3
|
module Snooby
|
6
|
-
|
7
|
-
|
4
|
+
# Opens a persistent connection that provides a significant speed improvement
|
5
|
+
# during repeated calls; reddit's rate limit nullifies this for the most part,
|
6
|
+
# but it's still a nice library and persistent connections are a Good Thing.
|
7
|
+
Conn = Net::HTTP::Persistent.new('snooby')
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
# Provides a mapping of things and actions to their respective URL fragments.
|
10
|
+
# A path is eventually used as a complete URI, thus the merge.
|
11
|
+
paths = {
|
12
|
+
:comment => 'api/comment',
|
13
|
+
:login => 'api/login/%s',
|
14
|
+
:me => 'api/me.json',
|
15
|
+
:subreddit_about => 'r/%s/about.json',
|
16
|
+
:subreddit_comments => 'r/%s/comments.json',
|
17
|
+
:subreddit_posts => 'r/%s.json',
|
18
|
+
:user => 'user/%s',
|
19
|
+
:user_about => 'user/%s/about.json',
|
20
|
+
:user_comments => 'user/%s/comments.json',
|
21
|
+
:user_posts => 'user/%s/submitted.json',
|
22
|
+
}
|
23
|
+
Paths = paths.merge(paths) { |k, v| 'http://www.reddit.com/' + v }
|
12
24
|
|
13
|
-
|
25
|
+
# Provides a mapping of things to a list of all the attributes present in the
|
26
|
+
# relevant JSON object. A lot of these probably won't get used too often, but
|
27
|
+
# might as well expose all available data.
|
28
|
+
Fields = {
|
29
|
+
:comment => %w[author author_flair_css_class author_flair_text body body_html created created_utc downs id likes link_id link_title name parent_id replies subreddit subreddit_id ups],
|
30
|
+
:post => %w[author author_flair_css_class author_flair_text clicked created created_utc domain downs hidden id is_self likes media media_embed name num_comments over_18 permalink saved score selftext selftext_html subreddit subreddit_id thumbnail title ups url]
|
31
|
+
}
|
32
|
+
|
33
|
+
# The crux of Snooby. Generates an array of structs from the Paths and Fields
|
34
|
+
# hashes defined above. In addition to just being a very neat container, this
|
35
|
+
# allows accessing the returned JSON values using thing.attribute, as opposed
|
36
|
+
# to thing['data']['attribute']. Only used for listings of posts and comments
|
37
|
+
# at the moment, but I imagine it'll be used for moderation down the road.
|
38
|
+
def self.build(object, path, which)
|
39
|
+
# A bit of string manipulation to determine which fields to populate the
|
40
|
+
# generated struct with. There might be a less fragile way to go about it,
|
41
|
+
# but it shouldn't be a problem as long as naming remains consistent.
|
42
|
+
kind = object.to_s.split('::')[1].downcase.to_sym
|
43
|
+
|
44
|
+
# Having to explicitly pass the path symbol isn't exactly DRY, but deriving
|
45
|
+
# it from the object parameter (say, Snooby::Comment) doesn't expose which
|
46
|
+
# kind of comment it is, either User or Post.
|
47
|
+
uri = URI(Paths[path] % which)
|
48
|
+
|
49
|
+
# This'll likely have to be tweaked to handle other types of listings, but
|
50
|
+
# it's sufficient for comments and posts.
|
51
|
+
JSON.parse(Conn.request(uri).body)['data']['children'].map do |child|
|
52
|
+
# Maps each of the listing's children to the relevant struct based on the
|
53
|
+
# object type passed in. The symbols in a struct definition are ordered,
|
54
|
+
# but Python dictionaries are not, so #values isn't sufficient.
|
55
|
+
object.new(*child['data'].values_at(*Fields[kind]))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
attr_accessor :config, :auth
|
61
|
+
end
|
62
|
+
|
63
|
+
# Used for permanent storage of preferences and authorization data.
|
64
|
+
# Each client should have its own directory to prevent pollution.
|
65
|
+
@config = JSON.parse(File.read('.snooby')) rescue {'auth' => {}}
|
66
|
+
|
67
|
+
# Raised with a pretty print of the relevant JSON object whenever an API call
|
68
|
+
# returns a non-empty "errors" field. Where "pretty" is inapplicable, such as
|
69
|
+
# when the returned JSON object is a series of jQuery calls, a manual message
|
70
|
+
# is displayed instead, typically to inform the user either that they've been
|
71
|
+
# rate-limited or that they lack the necessary authorization.
|
72
|
+
class RedditError < StandardError; end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Snooby's parts are required down here, after its initial declaration, because
|
76
|
+
# Post and Comment are structs whose definitions are taken from the Fields hash
|
77
|
+
# above, and related bits might as well be kept together.
|
78
|
+
%w[client actions objects].each do |d|
|
79
|
+
require "snooby/#{d}"
|
14
80
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Snooby
|
2
|
+
# Mixin to provide functionality to all of the objects in one fell swoop,
|
3
|
+
# both relevant and irrelevant. Errors are thrown where nonsensical methods
|
4
|
+
# are called, but hopefully the intent is to use Snooby sensibly. I realize
|
5
|
+
# this is probably bad design, but it's just so damned clean.
|
6
|
+
module Actions
|
7
|
+
# Returns a hash containing the values supplied by about.json, so long as
|
8
|
+
# the calling object is a User or Subreddit.
|
9
|
+
def about
|
10
|
+
if !['user', 'subreddit'].include?(@kind)
|
11
|
+
raise RedditError, 'Only users and subreddits have about pages.'
|
12
|
+
end
|
13
|
+
|
14
|
+
uri = URI(Paths[:"#{@kind}_about"] % @name)
|
15
|
+
JSON.parse(Conn.request(uri).body)['data']
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an array of structs containing the object's posts.
|
19
|
+
def posts
|
20
|
+
if !['user', 'subreddit'].include?(@kind)
|
21
|
+
raise RedditError, 'Only users and subreddits have posts.'
|
22
|
+
end
|
23
|
+
|
24
|
+
Snooby.build(Post, :"#{@kind}_posts", @name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns an array of structs containing the object's comments.
|
28
|
+
def comments
|
29
|
+
if !['user', 'subreddit'].include?(@kind)
|
30
|
+
raise RedditError, 'Only users and subreddits have comments.'
|
31
|
+
end
|
32
|
+
|
33
|
+
Snooby.build(Comment, :"#{@kind}_comments", @name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns an array of 2-tuples containing the user's trophy information in
|
37
|
+
# the form of [name, description], the latter containing the empty string
|
38
|
+
# if inapplicable.
|
39
|
+
def trophies
|
40
|
+
raise RedditError, 'Only users have trophies.' if @kind != 'user'
|
41
|
+
|
42
|
+
# Only interested in trophies; request the minimum amount of content.
|
43
|
+
html = Conn.request(URI(Paths[:user] % @name) + '?limit=1').body
|
44
|
+
# Entry-level black magic.
|
45
|
+
html.scan(/"trophy-name">(.+?)<.+?"\s?>([^<]*)</)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Posts a reply to the caller as the currently authorized user, so long as
|
49
|
+
# that caller is a Post or Comment.
|
50
|
+
def reply(text)
|
51
|
+
raise RedditError, 'You must be authorized to comment.' if !Snooby.auth
|
52
|
+
|
53
|
+
if !['post', 'comment'].include?(@kind)
|
54
|
+
raise RedditError, "Replying to a #{@kind} doesn't make much sense."
|
55
|
+
end
|
56
|
+
|
57
|
+
uri = URI(Paths[:comment])
|
58
|
+
post = Net::HTTP::Post.new(uri.path)
|
59
|
+
data = {:parent => self.name, :text => text, :uh => Snooby.auth}
|
60
|
+
post.set_form_data(data)
|
61
|
+
json = JSON.parse(Conn.request(uri, post).body)['jquery']
|
62
|
+
|
63
|
+
# Bad magic numbers, I know, but getting rate-limited during a reply
|
64
|
+
# returns a bunch of serialized jQuery rather than a straightforward
|
65
|
+
# and meaningful message. Fleshing out errors is on the to-do list.
|
66
|
+
raise RedditError, json[14][3] if json.size == 17
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Snooby
|
2
|
+
# Interface through which Snooby does all of its interacting with the API.
|
3
|
+
class Client
|
4
|
+
def initialize(user_agent = 'Snooby')
|
5
|
+
# Net::HTTP's default User-Agent, "Ruby", is banned on reddit due to its
|
6
|
+
# frequent improper use; cheers to Eric Hodel (drbrain) for implementing
|
7
|
+
# this workaround for net-http-persistent.
|
8
|
+
Conn.override_headers['User-Agent'] = user_agent
|
9
|
+
end
|
10
|
+
|
11
|
+
# Causes the client to be recognized as the given user during API calls.
|
12
|
+
# GET operations do not need to be authorized, so if your intent is simply
|
13
|
+
# to gather data, feel free to disregard this method entirely.
|
14
|
+
def authorize!(user, passwd, force_update = false)
|
15
|
+
if Snooby.config['auth'][user] && !force_update
|
16
|
+
# Authorization data exists, skip login and potential rate-limiting.
|
17
|
+
@modhash, @cookie = Snooby.config['auth'][user]
|
18
|
+
else
|
19
|
+
uri = URI(Paths[:login] % user)
|
20
|
+
post = Net::HTTP::Post.new(uri.path)
|
21
|
+
data = {:user => user, :passwd => CGI.escape(passwd), :api_type => 'json'}
|
22
|
+
post.set_form_data(data)
|
23
|
+
json = JSON.parse(Conn.request(uri, post).body)['json']
|
24
|
+
|
25
|
+
# Will fire for incorrect login credentials and when rate-limited.
|
26
|
+
raise RedditError, jj(json) if !json['errors'].empty?
|
27
|
+
|
28
|
+
# Parse authorization data and store it both in the current config, as
|
29
|
+
# well as in the configuration file for future use. This works because
|
30
|
+
# authorization data is immortal unless the password has changed. The
|
31
|
+
# force_update parameter should be enabled if such is the case.
|
32
|
+
@modhash, @cookie = json['data'].values
|
33
|
+
Snooby.config['auth'][user] = [@modhash, @cookie]
|
34
|
+
File.open('.snooby', 'w') { |f| f << Snooby.config.to_json }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets the reddit_session cookie required for API calls to be recognized
|
38
|
+
# as coming from the intended user; uses override_headers to allow for
|
39
|
+
# switching the current user mid-execution, if so desired.
|
40
|
+
Conn.headers['Cookie'] = "reddit_session=#{@cookie}"
|
41
|
+
|
42
|
+
# Allows Snooby's classes to access the currently authorized client.
|
43
|
+
Snooby.auth = @modhash
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a hash containing the values supplied by me.json.
|
47
|
+
def me
|
48
|
+
uri = URI(Paths[:me])
|
49
|
+
JSON.parse(Conn.request(uri).body)['data']
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a User object from which posts, comments, etc. can be accessed.
|
53
|
+
def user(name)
|
54
|
+
User.new(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# As above, so below.
|
58
|
+
def subreddit(name)
|
59
|
+
Subreddit.new(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
alias :u :user
|
63
|
+
alias :r :subreddit
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Snooby
|
2
|
+
class User
|
3
|
+
include Actions
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
@kind = 'user'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Subreddit
|
12
|
+
include Actions
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
@kind = 'subreddit'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Post < Struct.new(*Fields[:post].map(&:to_sym))
|
21
|
+
include Actions
|
22
|
+
|
23
|
+
def initialize(*)
|
24
|
+
super
|
25
|
+
@kind = 'post'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Comment < Struct.new(*Fields[:comment].map(&:to_sym))
|
30
|
+
include Actions
|
31
|
+
|
32
|
+
def initialize(*)
|
33
|
+
super
|
34
|
+
@kind = 'comment'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,52 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: snooby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
-
-
|
8
|
+
- Donnie Akers
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
13
|
-
dependencies:
|
14
|
-
|
12
|
+
date: 2012-02-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &79734590 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *79734590
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: net-http-persistent
|
27
|
+
requirement: &79734280 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.5'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *79734280
|
36
|
+
description:
|
15
37
|
email:
|
16
38
|
- andkerosine@gmail.com
|
17
39
|
executables: []
|
18
40
|
extensions: []
|
19
41
|
extra_rdoc_files: []
|
20
42
|
files:
|
21
|
-
- .gitignore
|
22
43
|
- Gemfile
|
44
|
+
- README.md
|
23
45
|
- Rakefile
|
24
46
|
- lib/snooby.rb
|
25
|
-
- lib/snooby/
|
26
|
-
- snooby.
|
47
|
+
- lib/snooby/actions.rb
|
48
|
+
- lib/snooby/client.rb
|
49
|
+
- lib/snooby/objects.rb
|
27
50
|
homepage: https://github.com/andkerosine/snooby
|
28
51
|
licenses: []
|
29
52
|
post_install_message:
|
@@ -43,9 +66,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
66
|
- !ruby/object:Gem::Version
|
44
67
|
version: '0'
|
45
68
|
requirements: []
|
46
|
-
rubyforge_project:
|
69
|
+
rubyforge_project:
|
47
70
|
rubygems_version: 1.8.15
|
48
71
|
signing_key:
|
49
72
|
specification_version: 3
|
50
|
-
summary:
|
73
|
+
summary: Snooby wraps the reddit API in happy, convenient Ruby.
|
51
74
|
test_files: []
|
data/.gitignore
DELETED
data/lib/snooby/version.rb
DELETED
data/snooby.gemspec
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
3
|
-
require 'snooby/version'
|
4
|
-
|
5
|
-
Gem::Specification.new do |s|
|
6
|
-
s.name = 'snooby'
|
7
|
-
s.version = Snooby::VERSION
|
8
|
-
s.authors = ['andkerosine']
|
9
|
-
s.email = ['andkerosine@gmail.com']
|
10
|
-
s.homepage = 'https://github.com/andkerosine/snooby'
|
11
|
-
s.summary = 'Ruby wrapper for the reddit API.'
|
12
|
-
s.description = 'Snooby wraps the reddit API in convenient Ruby.'
|
13
|
-
|
14
|
-
s.rubyforge_project = 'snooby'
|
15
|
-
|
16
|
-
s.files = `git ls-files`.split("\n")
|
17
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
-
s.require_paths = ["lib"]
|
20
|
-
end
|