Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
robertodecurnex committed Dec 29, 2021
0 parents commit fd8ea2b
Show file tree
Hide file tree
Showing 24 changed files with 818 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.gem
*.swp
.DS_Store
Gemfile.lock
coverage/*
10 changes: 10 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
AllCops:
NewCops: enable
Style/OpenStructUse:
Enabled: false
Metrics/MethodLength:
Exclude:
- 'test/**/*'
Layout/LineLength:
Exclude:
- 'test/**/*'
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.7.5
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

gem 'rubocop', '~> 1.23'

gem 'simplecov', '~> 0.21.2', require: false, group: :test
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Twttr
Twitter API v2 Interface

## Summery
Modular Twitter API interface, initially targeting Twitter API v2

## Draft Interface Examples

### Config
```ruby
# Creating a Client using OAuth 1.0a User context
client = Twttr::Client.new(user_id) do |config|
# App credentials
config.consumer_key = "consumer_key"
config.consumer_secret = "consumer_secret"

# User credentials
config.access_token = "access_token"
config.access_token_secret = "access_secret"

# Default user fields
# https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/user
config.user_fields = %w(id, name, username)
end #=> #<Twttr::Client>
```

### Client
```ruby
# Twttr::Client#me
client.me #=> #<Twttr::Model::User>

#Twttr::Client#user(:user_id)
client.user("user_id") #=> #<Twttr::Model::User>

#Twttr::Client#user_by_username(:username)
client.user_by_username("username") #=> #<Twttr::Model::User>

#Twttr::Client#users(:user_ids)
client.users(["user_id_1", "user_id_2"]) #=> [#<Twttr::Model::User>]
```
### User
```ruby
# Twttr::Model::User#following
#
# Yields each page of users
user.following do |users, pagination_token|
users #=> [#<Twttr::Model::User>]
end

# First page of users
user.following #=> [#<Twttr::Model::User>], pagination_token
```
9 changes: 9 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require 'rake/testtask'

Rake::TestTask.new do |t|
t.libs << 'test'
t.test_files = FileList['test/**/test_*.rb']
t.verbose = true
end
8 changes: 8 additions & 0 deletions lib/twttr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

require 'twttr/client'
require 'twttr/model'

#  Library namespace
module Twttr
end
51 changes: 51 additions & 0 deletions lib/twttr/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'forwardable'
require 'json'
require 'net/http'
require 'oauth'
require 'oauth/request_proxy/net_http'
require 'ostruct'
require 'securerandom'
require 'uri/query_params'

require 'twttr/client/config'
require 'twttr/client/endpoint'
require 'twttr/client/error'
require 'twttr/client/oauth_request'

require 'uri/generic'

module Twttr
#  Twitter API Client
class Client
include Twttr::Client::Endpoint::V2::Users
include Twttr::Client::Endpoint::V2::Users::Follows

attr_reader :config

BASE_URL = 'https://api.twitter.com'

def initialize
@config = Config.new
yield config if block_given?
end

def get(path, params: {}, query_params: {})
uri = uri_for(path, params)
uri.query = URI.encode_www_form(query_params.compact) unless query_params.compact.empty?

response = OAuthRequest.get(uri, config)

JSON.parse(response.body)
end

private

def uri_for(path, params = {})
return URI.parse("#{BASE_URL}#{path}") if params.empty?

URI.parse("#{BASE_URL}#{path}" % params) # rubocop:disable Style/FormatString
end
end
end
15 changes: 15 additions & 0 deletions lib/twttr/client/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Twttr
class Client
# Client Configuration
class Config
attr_accessor :consumer_key, :consumer_secret, :access_token, :access_token_secret
attr_reader :user_fields

def user_fields=(fields)
@user_fields = fields.join(',')
end
end
end
end
11 changes: 11 additions & 0 deletions lib/twttr/client/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require 'twttr/client/endpoint/v2'

module Twttr
class Client
#  Namespace to keep endpoints organized by version and domain.
module Endpoint
end
end
end
13 changes: 13 additions & 0 deletions lib/twttr/client/endpoint/v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Twttr
class Client
module Endpoint
module V2
V2_PATH = '/2'
end
end
end
end

require_relative 'v2/users'
43 changes: 43 additions & 0 deletions lib/twttr/client/endpoint/v2/users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Twttr
class Client
module Endpoint
module V2
#  Twitter API V2 Users related endpoints
#  https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference
module Users
ME_PATH = "#{V2::V2_PATH}/users/me"
USERS_PATH = "#{V2::V2_PATH}/users"
USER_BY_USERNAME_PATH = "#{V2::V2_PATH}/users/by/username/%<username>s"
USER_PATH = "#{V2::V2_PATH}/users/%<user_id>s"

def me
response = get(ME_PATH, query_params: { 'user.fields': config.user_fields })
Model::User.new(response['data'], self)
end

def user(user_id)
response = get(USER_PATH, params: { user_id: user_id },
query_params: { 'user.fields': config.user_fields })
Model::User.new(response['data'], self)
end

def user_by_username(username)
response = get(USER_BY_USERNAME_PATH, params: { username: username },
query_params: { 'user.fields': config.user_fields })
Model::User.new(response['data'], self)
end

def users(user_ids)
response = get(USERS_PATH,
query_params: { ids: user_ids.join(','), 'user.fields': config.user_fields })
response['data'].map { |v| Model::User.new(v, self) }
end
end
end
end
end
end

require_relative 'users/follows'
47 changes: 47 additions & 0 deletions lib/twttr/client/endpoint/v2/users/follows.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module Twttr
class Client
module Endpoint
module V2
module Users
# Twitter API V2 User Follow related endpoints
# https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference
module Follows
FOLLOWING_PATH = "#{Users::USER_PATH}/following"

# GET /2/users/:id/following
# https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/get-users-id-following
#
# @param user_id [String] The user ID whose following you would like to retrieve.
# @param max_results [Integer] Max number of results per peage.
# @param pagination_token [String] Initial page pagination token.
# @yield [Array<Twttr::Model::User>] Users followed by page.
# @return [Array<Twttr::Model::User>] Users followed.
# @return [String,NilClass] Pagination token.
def following(user_id, max_results: nil, pagination_token: nil) # rubocop:disable Metrics/MethodLength
loop do
response = get(FOLLOWING_PATH, params: { user_id: user_id },
query_params: {
'user.fields': config.user_fields,
max_results: max_results,
pagination_token: pagination_token
}.compact)

users = response['data'].map { |v| Model::User.new(v, self) }

pagination_token = response['meta']['pagination_token']

return users, pagination_token unless block_given?

yield users, pagination_token

break if pagination_token.nil?
end
end
end
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/twttr/client/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Twttr
class Client
#  Client rrrors namesace
module Error
#  HTTP related errors
class HTTPError < StandardError
def initialize(msg = 'HTTP Error')
super
end
end
end
end
end
84 changes: 84 additions & 0 deletions lib/twttr/client/oauth_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Twttr
class Client
#  OAuth helper methods
class OAuthRequest
attr_reader :response

def initialize(uri, config)
@uri = uri
@config = config
@request = Net::HTTP::Get.new(uri)
@request['Authorization'] = authorization_header
@response = nil
end

def self.get(uri, config)
new(uri, config).perform
end

def perform
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
# :nocov:
self.response = http.request request
# :nocov:
end

raise Error::HTTPError, response.message unless response.instance_of?(Net::HTTPOK)

response
end

private

attr_reader :config, :request, :uri
attr_writer :response

def oauth_params
@oauth_params ||= {
'oauth_consumer_key' => CGI.escape(config.consumer_key),
'oauth_nonce' => CGI.escape(SecureRandom.urlsafe_base64(32)),
'oauth_signature_method' => CGI.escape('HMAC-SHA1'),
'oauth_timestamp' => CGI.escape(Time.now.to_i.to_s),
'oauth_token' => CGI.escape(config.access_token),
'oauth_version' => CGI.escape('1.0')
}
end

def request_params
@request_params if defined?(@request_params)

query_params = {}
uri.query_params.each_pair do |key, value|
query_params[CGI.escape(key)] = CGI.escape(value)
end

@request_params = oauth_params.merge(query_params)
end

def authorization_header
oauth_params['oauth_signature'] = oauth_signature

serialized_params = oauth_params.keys.sort.map { |k| "#{k}=\"#{oauth_params[k]}\"" }.join(',')

"OAuth #{serialized_params}"
end

def oauth_signature
signature_params = request_params.keys.sort.map { |k| "#{k}=#{request_params[k]}" }.join('&')

base_string = "#{request.method}&#{CGI.escape(request.uri.to_s.sub(/\?.*$/,
''))}&#{CGI.escape(signature_params)}"

auth_code(base_string)
end

def auth_code(base_string)
signin_key = "#{CGI.escape(config.consumer_secret)}&#{CGI.escape(config.access_token_secret)}"

CGI.escape(Base64.encode64(OpenSSL::HMAC.digest('sha1', signin_key, base_string).to_s).chomp)
end
end
end
end
9 changes: 9 additions & 0 deletions lib/twttr/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require 'twttr/model/user'

module Twttr
# Ruby representations of Twitter entities
module Model
end
end
Loading

0 comments on commit fd8ea2b

Please sign in to comment.