Skip to content
4 changes: 4 additions & 0 deletions lib/convertkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
require 'convertkit/resources/sequence_response'
require 'convertkit/resources/sequences'
require 'convertkit/resources/subscriber_response'
require 'convertkit/resources/subscriber_bulk_add_tag_response'
require 'convertkit/resources/subscriber_bulk_create_response'
require 'convertkit/resources/subscriber_bulk_create_failure_response'
require 'convertkit/resources/subscriber_bulk_remove_tag_response'
require 'convertkit/resources/subscriber_bulk_tag_failure_response'
require 'convertkit/resources/subscriber_tag_response'
require 'convertkit/resources/subscribers'
require 'convertkit/resources/subscribers_response'
require 'convertkit/resources/subscription_response'
Expand Down
8 changes: 8 additions & 0 deletions lib/convertkit/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,12 @@ def handle_response(response, raw_response= false)
end
end
end

# This client behaves like the standard Client, but uses an API Key rather than OAuth access_token.
# Certain API resources can only be accessed with an OAuth access_token
class DeveloperClient < Client
def initialize(api_key)
@connection = ConvertKit::Connection.new(API_URL, api_key: api_key)
end
end
end
12 changes: 10 additions & 2 deletions lib/convertkit/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ class Connection
def initialize(url, options = {})
@url = url

@connection = Faraday.new(url: @url, headers: {'content-type' => MIME_TYPE }) do |builder|
builder.request :authorization, 'Bearer', options[:auth_token] if options[:auth_token]
@connection = Faraday.new(url: @url, headers: default_headers(options)) do |builder|
if options[:auth_token]
builder.request :authorization, 'Bearer', options[:auth_token]
end
end
end

Expand All @@ -32,6 +34,12 @@ def initialize(url, options = {})

private

def default_headers(options)
headers = { 'Content-Type' => MIME_TYPE }
headers['X-Kit-Api-Key'] = options[:api_key] if options[:api_key]
headers
end

def process_request(method, params)
return params if method == :get
return params if params.empty?
Expand Down
17 changes: 17 additions & 0 deletions lib/convertkit/resources/subscriber_bulk_add_tag_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module ConvertKit
module Resources
class SubscriberBulkAddTagResponse
attr_accessor :subscribers, :failures

def initialize(response)
@subscribers = response['subscribers'].map do |subscriber|
ConvertKit::Resources::TaggedSubscriberResponse.new(subscriber)
end

@failures = response['failures'].map do |failure|
ConvertKit::Resources::SubscriberBulkTagFailureResponse.new(failure)
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/convertkit/resources/subscriber_bulk_remove_tag_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module ConvertKit
module Resources
class SubscriberBulkRemoveTagResponse
attr_accessor :failures

def initialize(response)
@failures = response['failures'].map do |failure|
ConvertKit::Resources::SubscriberBulkTagFailureResponse.new(failure)
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/convertkit/resources/subscriber_bulk_tag_failure_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module ConvertKit
module Resources
class SubscriberBulkTagFailureResponse
attr_accessor :tagging, :errors

def initialize(response)
@tagging = SubscriberTagResponse.new(response['tagging']) if response['tagging']
@errors = response['errors']
end
end
end
end
12 changes: 12 additions & 0 deletions lib/convertkit/resources/subscriber_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@ def initialize(response)
@created_at = ConvertKit::Utils.to_datetime(response['created_at'])
end
end

class TaggedSubscriberResponse
attr_accessor :id, :first_name, :email, :created_at, :tagged_at

def initialize(response)
@id = response['id']
@first_name = response['first_name']
@email = response['email_address']
@created_at = ConvertKit::Utils.to_datetime(response['created_at'])
@tagged_at = ConvertKit::Utils.to_datetime(response['tagged_at'])
end
end
end
end
12 changes: 12 additions & 0 deletions lib/convertkit/resources/subscriber_tag_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module ConvertKit
module Resources
class SubscriberTagResponse
attr_accessor :tag_id, :subscriber_id

def initialize(response)
@tag_id = response['tag_id']
@subscriber_id = response['subscriber_id']
end
end
end
end
37 changes: 36 additions & 1 deletion lib/convertkit/resources/tags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ def list
def create(name)
response = @client.post(PATH, { name: name })

TagResponse.new(response)
TagResponse.new(response['tag'])
end

# Update the name of a Tag
# See https://developers.kit.com/v4#update-tag-name for details
# @param [Integer] tag_id
# @param [Hash] options
# @option options [String] :name New name for Tag
def update(tag_id, options = {})
request_options = options.slice(:name)
response = @client.put("#{PATH}/#{tag_id}", request_options)
TagResponse.new(response['tag'])
end

# Tags a subscriber
Expand Down Expand Up @@ -85,6 +96,30 @@ def subscriptions(tag_id, options = {})

SubscriptionsResponse.new(response)
end

# Bulk add tag to subscribers
# @param [Array<Hash>] taggings
# @option taggings [String] :tag_id
# @option taggings [String] :subscriber_id
def bulk_add_to_subscribers(taggings = [])
raise ArgumentError, 'taggings must be an array' unless taggings.is_a?(Array)

response = @client.post("bulk/#{PATH}/subscribers", { taggings: taggings })

ConvertKit::Resources::SubscriberBulkAddTagResponse.new(response)
end

# Bulk remove tag from subscribers
# @param [Array<Hash>] taggings
# @option taggings [String] :tag_id
# @option taggings [String] :subscriber_id
def bulk_remove_from_subscribers(taggings = [])
raise ArgumentError, 'taggings must be an array' unless taggings.is_a?(Array)

response = @client.delete("bulk/#{PATH}/subscribers", { taggings: taggings })

ConvertKit::Resources::SubscriberBulkRemoveTagResponse.new(response)
end
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/convertkit/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

describe '#initialize' do
it 'creates a new Faraday connection' do
expect(Faraday).to receive(:new).with(url: url, headers: {'content-type' => 'application/json' })
expect(Faraday).to receive(:new).with(url: url, headers: {'Content-Type' => 'application/json' })
ConvertKit::Connection.new(url)
end
end
Expand Down
13 changes: 13 additions & 0 deletions spec/lib/convertkit/developer_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe ConvertKit::DeveloperClient do
let(:connection) { double('connection') }

describe '#initialize' do
it 'sets the api_key' do
expect(ConvertKit::Connection).to receive(:new)
.with(ConvertKit::Client::API_URL, api_key: 'test_token')
.and_return(connection)
client = ConvertKit::DeveloperClient.new('test_token')
expect(client.instance_variable_get(:@connection)).to eq(connection)
end
end
end
100 changes: 98 additions & 2 deletions spec/lib/convertkit/resources/tags_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@

context 'when name is a string' do
it 'creates a tag' do
response = { 'id' => 1, 'name' => 'tag_name', account_id: 1, state: 'available', 'created_at' => '2023-08-09T04:30:00Z', updated_at: '2023-08-09T04:30:00Z'}
response = {'tag' => { 'id' => 1, 'name' => 'tag_name', account_id: 1, state: 'available', 'created_at' => '2023-08-09T04:30:00Z', updated_at: '2023-08-09T04:30:00Z'}}
expect(client).to receive(:post).with('tags', {name: 'tag_name'}).and_return(response)
tags_response = tags.create('tag_name')
validate_tag(tags_response, response)
validate_tag(tags_response, response['tag'])
end
end
end
Expand Down Expand Up @@ -142,4 +142,100 @@
validate_subscriptions(tag_subscriptions_response, response)
end
end

describe '#bulk_add_to_subscribers' do
let(:tags) { ConvertKit::Resources::Tags.new(client) }
let(:taggings) { [{'tag_id' => 1, 'subscriber_id' => 1}] }
let(:response) do
{
'subscribers' => [{
'id' => 1,
'first_name' => 'foo',
'email_address' => 'foo@bar.com',
'created_at' => '2023-08-09T04:30:00Z',
'tagged_at' => '2023-08-09T04:30:00Z'
}],
'failures' => []
}
end

context 'with taggings provided' do
it 'tags listed subscribers' do
expect(client).to receive(:post).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response)

tags_response = tags.bulk_add_to_subscribers(taggings)
validate_tagged_subscribers(tags_response, response)
end
end

# Failures are not well documented in the API documentation
context 'with failures' do
let(:response) do
{
'subscribers' => [],
'failures' => [{
'tagging' => {
'subscriber_id' => 1,
'tag_id' => 2,
},
'errors' => ['Test error message']
}]
}
end

it 'return failures with subscriber and error message' do
expect(client).to receive(:post).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response)

tags_response = tags.bulk_add_to_subscribers(taggings)
expect(tags_response.failures.count).to eq(1)
expect(tags_response.failures.first.tagging.subscriber_id).to eq(1)
expect(tags_response.failures.first.tagging.tag_id).to eq(2)
expect(tags_response.failures.first.errors.first).to eq('Test error message')
end
end
end

describe '#bulk_remove_from_subscribers' do
let(:tags) { ConvertKit::Resources::Tags.new(client) }
let(:taggings) { [{'tag_id' => 1, 'subscriber_id' => 1}] }
let(:response) do
{
'failures' => []
}
end

context 'with taggings provided' do
it 'return no failures' do
expect(client).to receive(:delete).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response)

tags_response = tags.bulk_remove_from_subscribers(taggings)
expect(tags_response.failures).to be_empty
end
end

# Failures are not well documented in the API documentation
context 'with failures' do
let(:response) do
{
'failures' => [{
'tagging' => {
'subscriber_id' => 1,
'tag_id' => 2,
},
'errors' => ['Test error message']
}]
}
end

it 'return failures with subscriber and error message' do
expect(client).to receive(:delete).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response)

tags_response = tags.bulk_remove_from_subscribers(taggings)
expect(tags_response.failures.count).to eq(1)
expect(tags_response.failures.first.tagging.subscriber_id).to eq(1)
expect(tags_response.failures.first.tagging.tag_id).to eq(2)
expect(tags_response.failures.first.errors.first).to eq('Test error message')
end
end
end
end
12 changes: 12 additions & 0 deletions spec/validators/subscriber_validators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ def validate_subscribers(subscribers, values)
end
end

def validate_tagged_subscribers(subscribers, values)
subscribers.subscribers.each_with_index do |subscriber, index|
values = values['subscribers'][index]

expect(subscriber.id).to eq(values['id'])
expect(subscriber.first_name).to eq(values['first_name'])
expect(subscriber.email).to eq(values['email_address'])
expect(subscriber.created_at).to eq(DateTime.parse(values['created_at'])) unless values.fetch('created_at', '').strip.empty?
expect(subscriber.tagged_at).to eq(DateTime.parse(values['tagged_at'])) unless values.fetch('tagged_at', '').strip.empty?
end
end

def validate_bulk_create_failure(bulk_create_failure_response, values)
expect(bulk_create_failure_response.errors).to match_array(values['errors']) if values['errors']
validate_subscriber(bulk_create_failure_response.subscriber, values['subscriber']) if values['subscriber']
Expand Down
Loading