nakajima-twitter-client 0.0.2
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/lib/core_ext/delegation.rb +25 -0
- data/lib/twitter/api.rb +13 -0
- data/lib/twitter/connection.rb +52 -0
- data/lib/twitter/session.rb +46 -0
- data/lib/twitter/store.rb +52 -0
- data/lib/twitter/twitter.rb +77 -0
- data/lib/twitter-client.rb +49 -0
- metadata +61 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
module Delegation
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ClassMethods
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
def delegate(*args)
|
|
9
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
10
|
+
if target = opts[:to]
|
|
11
|
+
args.each do |sym|
|
|
12
|
+
class_eval(<<-END, __FILE__, __LINE__)
|
|
13
|
+
def #{sym}(*args, &block)
|
|
14
|
+
send(#{target.inspect}) \
|
|
15
|
+
.send(#{sym.inspect}, *args, &block)
|
|
16
|
+
end
|
|
17
|
+
END
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "You must pass a target as :to option"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/twitter/api.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
# Namespace for constants and exceptions related to the Twitter API.
|
|
3
|
+
module API
|
|
4
|
+
# Everything goes to Twitter for now
|
|
5
|
+
BASE = 'twitter.com'
|
|
6
|
+
|
|
7
|
+
# Everything goes through JSON for now
|
|
8
|
+
FORMAT = 'json'
|
|
9
|
+
|
|
10
|
+
# Raised when API returns a 401 response status.
|
|
11
|
+
class Unauthorized < StandardError ; end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
class Connection
|
|
3
|
+
include Delegation
|
|
4
|
+
|
|
5
|
+
attr_reader :session
|
|
6
|
+
|
|
7
|
+
delegate :username, :password, :to => :session
|
|
8
|
+
|
|
9
|
+
def initialize(session)
|
|
10
|
+
@session = session
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Make GET request with HTTP auth
|
|
14
|
+
def get(path, params={})
|
|
15
|
+
request(:get, path, params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Make post request with HTTP auth
|
|
19
|
+
def post(path, params={})
|
|
20
|
+
request(:post, path, params) do |req|
|
|
21
|
+
req.set_form_data(params)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Ensure authentication credentials are valid
|
|
26
|
+
def authenticate!
|
|
27
|
+
@response ||= get('/account/verify_credentials')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Wrap common request logic for POST/GET requests
|
|
33
|
+
def request(verb, path, params)
|
|
34
|
+
path = path + '.' + Twitter::API::FORMAT
|
|
35
|
+
res = Net::HTTP.start(Twitter::API::BASE) do |http|
|
|
36
|
+
req = Net::HTTP.const_get(verb.to_s.capitalize).new(path)
|
|
37
|
+
req.basic_auth username, password
|
|
38
|
+
yield req if block_given?
|
|
39
|
+
res = http.request(req)
|
|
40
|
+
check_response(res)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Ensure response is valid
|
|
45
|
+
def check_response(res)
|
|
46
|
+
case res
|
|
47
|
+
when Net::HTTPUnauthorized then raise API::Unauthorized.new
|
|
48
|
+
else res
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
# Most of the logic in twitter-client lives in here. Responsible
|
|
3
|
+
# for maintaining/persisting credentials.
|
|
4
|
+
class Session
|
|
5
|
+
attr_reader :username, :password
|
|
6
|
+
attr_writer :filename
|
|
7
|
+
|
|
8
|
+
def initialize(username, password, opts={})
|
|
9
|
+
@username, @password = username, password
|
|
10
|
+
@connection = opts[:connected]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Post a via the current session.
|
|
14
|
+
# TODO The Session class should be infrastructural. Domain logic like
|
|
15
|
+
# this should go elsewhere.
|
|
16
|
+
def tweet!(msg)
|
|
17
|
+
connection.post('/statuses/update', :status => msg) ; msg
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Ensure validity of credentials
|
|
21
|
+
def connect!
|
|
22
|
+
connection(true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Provide verification that session is able to be used
|
|
26
|
+
def connected?
|
|
27
|
+
if @connection
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
[username, password] == [other.username, other.password]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def connection(force=false)
|
|
39
|
+
(@connection and not force) ? @connection : begin
|
|
40
|
+
@connection = Connection.new(self)
|
|
41
|
+
@connection.authenticate!
|
|
42
|
+
@connection
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
class Store
|
|
3
|
+
attr_writer :filename
|
|
4
|
+
|
|
5
|
+
def initialize(env={})
|
|
6
|
+
@env = env
|
|
7
|
+
@sessions = @env[:sessions]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Persist the current session to a dot file
|
|
11
|
+
def save
|
|
12
|
+
f = File.new(File.join(ENV['HOME'], filename), 'w+')
|
|
13
|
+
f << yaml_dump
|
|
14
|
+
f.close
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Attempt to load the current session from a dot file
|
|
18
|
+
def load
|
|
19
|
+
YAML.load_file(File.join(ENV['HOME'], filename)) || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Allow custom name for dot file used to persist sessions
|
|
23
|
+
def filename
|
|
24
|
+
@filename || '.twitter'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sessions
|
|
28
|
+
@sessions ||= begin
|
|
29
|
+
load[:sessions].inject({}) do |res, (key, val)|
|
|
30
|
+
password = crypter.decrypt_string(val)
|
|
31
|
+
res[key] = Session.new(key, password, :connected => true)
|
|
32
|
+
res
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def crypter
|
|
40
|
+
@crypter ||= Crypt::Blowfish.new('twitter-crypt-key')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def yaml_dump
|
|
44
|
+
session_data = sessions.inject({}) do |res, session|
|
|
45
|
+
res[session.username] = crypter.encrypt_string(session.password)
|
|
46
|
+
res
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
{ :sessions => session_data }.to_yaml
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Twitter
|
|
2
|
+
class << self
|
|
3
|
+
# Reset session
|
|
4
|
+
def reset!
|
|
5
|
+
sessions.clear
|
|
6
|
+
@username = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def use(username)
|
|
10
|
+
@username = username
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def save(filename=nil)
|
|
14
|
+
store = Store.new(:sessions => sessions.values)
|
|
15
|
+
store.filename = filename if filename
|
|
16
|
+
store.save
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load(filename=nil)
|
|
20
|
+
store = Store.new
|
|
21
|
+
store.filename = filename if filename
|
|
22
|
+
if store.load
|
|
23
|
+
@sessions = store.sessions
|
|
24
|
+
@username = @sessions.keys.first
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Delegate method missing calls to session
|
|
29
|
+
def method_missing(sym, *args, &blk)
|
|
30
|
+
if authenticated? and delegating?(sym)
|
|
31
|
+
current_session.send(sym, *args, &blk)
|
|
32
|
+
else
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check for presence of session or try to load saved session
|
|
38
|
+
def authenticated?(loaded=false)
|
|
39
|
+
return true if current_session and current_session.connected?
|
|
40
|
+
if not loaded
|
|
41
|
+
load
|
|
42
|
+
authenticated?(true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Create new session from passed credentials
|
|
47
|
+
def authenticate(username, password)
|
|
48
|
+
session = Session.new(username, password)
|
|
49
|
+
session.connect!
|
|
50
|
+
sessions[username] = session
|
|
51
|
+
@username = username
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Load saved session from a .twitter file
|
|
55
|
+
def load_session
|
|
56
|
+
session = Session.new(nil, nil)
|
|
57
|
+
if session.load
|
|
58
|
+
@session = session ; true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def current_session
|
|
65
|
+
sessions[@username]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sessions
|
|
69
|
+
@sessions ||= {}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check to see if Twitter is delegating this method to session
|
|
73
|
+
def delegating?(sym)
|
|
74
|
+
Session.public_instance_methods.include?(sym.to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
|
2
|
+
|
|
3
|
+
# standard library
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
require 'cgi'
|
|
9
|
+
|
|
10
|
+
# Gems
|
|
11
|
+
begin
|
|
12
|
+
require 'crypt/blowfish'
|
|
13
|
+
rescue LoadError
|
|
14
|
+
$NO_CRYPT = true
|
|
15
|
+
# Fake encrypter if it's not available
|
|
16
|
+
module Crypt
|
|
17
|
+
class Blowfish
|
|
18
|
+
def initialize(*args) end
|
|
19
|
+
def decrypt_string(str); str end
|
|
20
|
+
def encrypt_string(str); str end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
puts " Warning!"
|
|
24
|
+
puts
|
|
25
|
+
puts " The crypt/blowfish gem was not found."
|
|
26
|
+
puts " As a result, saved session file will not be encrypted."
|
|
27
|
+
puts " Run `gem install crypt` to install encryption libraries."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Extensions
|
|
31
|
+
require 'core_ext/delegation'
|
|
32
|
+
|
|
33
|
+
# Source files
|
|
34
|
+
require 'twitter/api'
|
|
35
|
+
require 'twitter/store'
|
|
36
|
+
require 'twitter/session'
|
|
37
|
+
require 'twitter/connection'
|
|
38
|
+
require 'twitter/twitter'
|
|
39
|
+
|
|
40
|
+
# Hpricot-y constant-method
|
|
41
|
+
def Twitter(*args, &block)
|
|
42
|
+
if args.empty? and not block_given?
|
|
43
|
+
Twitter
|
|
44
|
+
else
|
|
45
|
+
if args.length == 1
|
|
46
|
+
Twitter.tweet!(*args)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nakajima-twitter-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pat Nakajima
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-03-30 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description:
|
|
17
|
+
email: patnakajima@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
|
|
24
|
+
files:
|
|
25
|
+
- lib/core_ext
|
|
26
|
+
- lib/core_ext/delegation.rb
|
|
27
|
+
- lib/twitter
|
|
28
|
+
- lib/twitter/api.rb
|
|
29
|
+
- lib/twitter/connection.rb
|
|
30
|
+
- lib/twitter/session.rb
|
|
31
|
+
- lib/twitter/store.rb
|
|
32
|
+
- lib/twitter/twitter.rb
|
|
33
|
+
- lib/twitter-client.rb
|
|
34
|
+
has_rdoc: true
|
|
35
|
+
homepage: http://github.com/nakajima/twitter-client
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: "0"
|
|
46
|
+
version:
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: "0"
|
|
52
|
+
version:
|
|
53
|
+
requirements: []
|
|
54
|
+
|
|
55
|
+
rubyforge_project:
|
|
56
|
+
rubygems_version: 1.2.0
|
|
57
|
+
signing_key:
|
|
58
|
+
specification_version: 2
|
|
59
|
+
summary: Simple, clean, redundant.
|
|
60
|
+
test_files: []
|
|
61
|
+
|