diff --git a/.gitignore b/.gitignore index 72e0bfb..6f4430a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /test/tmp /test/version_tmp /tmp +/.vscode/ # Environment variables .env diff --git a/lib/dialpad.rb b/lib/dialpad.rb index a2caeee..46b49c9 100644 --- a/lib/dialpad.rb +++ b/lib/dialpad.rb @@ -15,8 +15,12 @@ class APIError < Error; end require 'dialpad/call_center' require 'dialpad/contact' require 'dialpad/department' +require 'dialpad/sms' +require 'dialpad/meeting' +require 'dialpad/recording_sharelink' require 'dialpad/subscriptions/call_event' require 'dialpad/subscriptions/contact_event' +require 'dialpad/subscriptions/sms_event' require 'dialpad/user' require 'dialpad/websocket' diff --git a/lib/dialpad/meeting.rb b/lib/dialpad/meeting.rb new file mode 100644 index 0000000..31b40d0 --- /dev/null +++ b/lib/dialpad/meeting.rb @@ -0,0 +1,67 @@ +module Dialpad + class Meeting < DialpadObject + class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; end + + ATTRIBUTES = %i( + call_out + dial_in_number + duration + end_datetime + id + instance_id + is_moderated + meeting_type + meeting_url + organizer_pin + participant_pin + participants_info + recurrence + recurrence_end_date + start_datetime + timezone + title + ).freeze + + class << self + include Validations + + # https://developers.dialpad.com/reference/meetingsget + def retrieve(scheduled_conference_id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.get("meetings/#{scheduled_conference_id}") + new(response.body) + end + + # https://developers.dialpad.com/reference/meetingslist + def list(params = {}) + response = Dialpad.client.get('meetings', params) + paginated_response_from(response) + end + + # https://developers.dialpad.com/reference/meetingscreate + def create(attributes = {}) + validate_required_attributes(attributes, %i(end_datetime meeting_type start_datetime title user_id)) + + response = Dialpad.client.post('meetings', attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/meetingsupdate + def update(scheduled_conference_id = nil, attributes = {}) + validate_required_attributes(attributes, %i(end_datetime meeting_type start_datetime title user_id)) + + response = Dialpad.client.put("meetings/#{scheduled_conference_id}", attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/meetingsdelete + def destroy(scheduled_conference_id = nil) + validate_required_attribute(scheduled_conference_id, "Scheduled Conference ID") + + response = Dialpad.client.delete("meetings/#{scheduled_conference_id}") + new(response.body) + end + end + end +end diff --git a/lib/dialpad/recording_sharelink.rb b/lib/dialpad/recording_sharelink.rb new file mode 100644 index 0000000..98fcfd0 --- /dev/null +++ b/lib/dialpad/recording_sharelink.rb @@ -0,0 +1,56 @@ +module Dialpad + class RecordingSharelink < DialpadObject + class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; end + + ATTRIBUTES = %i( + access_link + call_id + created_by_id + date_added + id + item_id + privacy + type + ).freeze + + # privacy: (admin company owner public) + # type: (admincallrecording callrecording voicemail) + + class << self + include Validations + + # https://developers.dialpad.com/reference/recording_share_linkget + def retrieve(id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.get("recordingsharelink/#{id}") + new(response.body) + end + + # https://developers.dialpad.com/reference/contactscreate + def create(attributes = {}) + validate_required_attributes(attributes, %i(recording_id recording_type)) + + response = Dialpad.client.post('recordingsharelink', attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/recording_share_linkupdate + def update(id = nil, attributes = {}) + validate_required_attribute(id, "ID") + validate_required_attributes(attributes, %i(privacy)) + + response = Dialpad.client.put("recordingsharelink/#{id}", attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/recording_share_linkdelete + def destroy(id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.delete("recordingsharelink/#{id}") + new(response.body) + end + end + end +end diff --git a/lib/dialpad/sms.rb b/lib/dialpad/sms.rb new file mode 100644 index 0000000..700598e --- /dev/null +++ b/lib/dialpad/sms.rb @@ -0,0 +1,55 @@ +module Dialpad + class Sms < DialpadObject + class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; end + + ATTRIBUTES = %i( + contact + contact_id + created_date + device_type + direction + event_timestamp + from_number + id + is_internal + message_delivery_result + message_status + mms + mms_url + sender_id + target + text + text_content + to_number + ).freeze + + def contact_id + attributes[:contact_id] || contact && contact[:id] + end + + def contact + attributes[:contact] + end + + class << self + # https://developers.dialpad.com/reference/smssend + + # Attributes: + # + # channel_hashtag: string | null + # from_number: string | null + # infer_country_code: boolean | null (Defaults to false) + # media: string | null (Base64-encoded media attachment (will cause the message to be sent as MMS). (Max 500 KiB raw file size)) + # sender_group_id: int64 | null + # sender_group_type: string | null, Allowed: (callcenter, department, office) + # text: string | null + # to_numbers: array of strings | null + # user_id: int64 | null + + def send(attributes = {}) + response = Dialpad.client.post('sms', attributes) + new(response.body) + end + end + end +end diff --git a/lib/dialpad/subscriptions/sms_event.rb b/lib/dialpad/subscriptions/sms_event.rb new file mode 100644 index 0000000..37fc861 --- /dev/null +++ b/lib/dialpad/subscriptions/sms_event.rb @@ -0,0 +1,70 @@ +module Dialpad + module Subscriptions + class SmsEvent < DialpadObject + class RequiredAttributeError < Dialpad::DialpadObject::RequiredAttributeError; end + + ATTRIBUTES = %i( + direction + enabled + id + include_internal + status + target_id + target_type + webhook + websocket + ).freeze + + # target_type is one of (callcenter callrouter channel coachinggroup coachingteam department office room staffgroup unknown user) + # direction is one of (all inbound outbound) + + # Response might contain webhook or websocket object + def webhook + attributes[:webhook] + end + + def websocket + attributes[:websocket] + end + + class << self + include Validations + + # https://developers.dialpad.com/reference/webhook_sms_event_subscriptionget + def retrieve(id = nil) + validate_required_attribute(id, "ID") + response = Dialpad.client.get("subscriptions/sms/#{id}") + new(response.body) + end + + # https://developers.dialpad.com/reference/webhook_sms_event_subscriptionlist + def list(params = {}) + response = Dialpad.client.get('subscriptions/sms', params) + paginated_response_from(response) + end + + # https://developers.dialpad.com/reference/webhook_sms_event_subscriptioncreate + def create(attributes = {}) + validate_required_attributes(attributes, [:webhook_id]) + + response = Dialpad.client.post('subscriptions/sms', attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/webhook_sms_event_subscriptionupdate + def update(id = nil, attributes = {}) + response = Dialpad.client.patch("subscriptions/sms/#{id}", attributes) + new(response.body) + end + + # https://developers.dialpad.com/reference/webhook_sms_event_subscriptiondelete + def destroy(id = nil) + validate_required_attribute(id, "ID") + + response = Dialpad.client.delete("subscriptions/sms/#{id}") + new(response.body) + end + end + end + end +end diff --git a/spec/dialpad/recording_sharelink_spec.rb b/spec/dialpad/recording_sharelink_spec.rb new file mode 100644 index 0000000..3fac9a4 --- /dev/null +++ b/spec/dialpad/recording_sharelink_spec.rb @@ -0,0 +1,382 @@ +require 'spec_helper' + +RSpec.describe Dialpad::RecordingSharelink do + let(:base_url) { 'https://api.dialpad.com' } + let(:token) { 'test_token' } + let(:client) { Dialpad::Client.new(base_url: base_url, token: token) } + + before do + allow(Dialpad).to receive(:client).and_return(client) + end + + describe 'class methods' do + describe '.retrieve' do + context 'with valid ID' do + let(:recording_sharelink_data) do + { + 'access_link' => 'https://dialpad.com/recording/admincallrecording/testLinkId123456789', + 'call_id' => '1234567890123456', + 'created_by_id' => '9876543210987654', + 'date_added' => '2026-02-03T10:44:38.988809', + 'id' => 'testLinkId123456789', + 'item_id' => '5555666677778888', + 'privacy' => 'company', + 'type' => 'admincallrecording' + } + end + + it 'retrieves a recording sharelink by ID' do + stub_request(:get, "#{base_url}/recordingsharelink/test123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: recording_sharelink_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + sharelink = described_class.retrieve('test123') + + expect(sharelink).to be_a(described_class) + expect(sharelink.id).to eq('testLinkId123456789') + expect(sharelink.access_link).to eq('https://dialpad.com/recording/admincallrecording/testLinkId123456789') + expect(sharelink.call_id).to eq('1234567890123456') + expect(sharelink.created_by_id).to eq('9876543210987654') + expect(sharelink.date_added).to eq('2026-02-03T10:44:38.988809') + expect(sharelink.item_id).to eq('5555666677778888') + expect(sharelink.privacy).to eq('company') + expect(sharelink.type).to eq('admincallrecording') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.retrieve(nil) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.retrieve('') }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + end + + describe '.create' do + context 'with valid attributes' do + let(:create_attributes) do + { + recording_id: '1234567890123456', + recording_type: 'admincallrecording' + } + end + + let(:created_sharelink_data) do + { + 'access_link' => 'https://dialpad.com/recording/admincallrecording/newTestLinkId987654321', + 'call_id' => '1234567890123456', + 'created_by_id' => '9876543210987654', + 'date_added' => '2026-02-03T11:30:15.123456', + 'id' => 'newTestLinkId987654321', + 'item_id' => '1234567890123456', + 'privacy' => 'company', + 'type' => 'admincallrecording' + } + end + + it 'creates a new recording sharelink' do + stub_request(:post, "#{base_url}/recordingsharelink") + .with( + headers: { 'Authorization' => "Bearer #{token}" }, + body: create_attributes.to_json + ) + .to_return(status: 201, body: created_sharelink_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + sharelink = described_class.create(create_attributes) + + expect(sharelink).to be_a(described_class) + expect(sharelink.id).to eq('newTestLinkId987654321') + expect(sharelink.access_link).to eq('https://dialpad.com/recording/admincallrecording/newTestLinkId987654321') + expect(sharelink.call_id).to eq('1234567890123456') + expect(sharelink.created_by_id).to eq('9876543210987654') + expect(sharelink.item_id).to eq('1234567890123456') + expect(sharelink.privacy).to eq('company') + expect(sharelink.type).to eq('admincallrecording') + end + end + + context 'with missing required attributes' do + it 'raises RequiredAttributeError when recording_id is missing' do + expect { described_class.create({ recording_type: 'admincallrecording' }) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attributes: recording_id' + ) + end + + it 'raises RequiredAttributeError when recording_type is missing' do + expect { described_class.create({ recording_id: '123' }) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attributes: recording_type' + ) + end + + it 'raises RequiredAttributeError when both required attributes are missing' do + expect { described_class.create({}) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attributes: recording_id, recording_type' + ) + end + end + end + + describe '.update' do + context 'with valid ID and attributes' do + let(:update_attributes) do + { + privacy: 'public' + } + end + + let(:updated_sharelink_data) do + { + 'access_link' => 'https://dialpad.com/recording/admincallrecording/testLinkId123456789', + 'call_id' => '1234567890123456', + 'created_by_id' => '9876543210987654', + 'date_added' => '2026-02-03T10:44:38.988809', + 'id' => 'testLinkId123456789', + 'item_id' => '5555666677778888', + 'privacy' => 'public', + 'type' => 'admincallrecording' + } + end + + it 'updates a recording sharelink' do + stub_request(:put, "#{base_url}/recordingsharelink/test123") + .with( + headers: { 'Authorization' => "Bearer #{token}" }, + body: update_attributes.to_json + ) + .to_return(status: 200, body: updated_sharelink_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + sharelink = described_class.update('test123', update_attributes) + + expect(sharelink).to be_a(described_class) + expect(sharelink.id).to eq('testLinkId123456789') + expect(sharelink.privacy).to eq('public') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.update(nil, {}) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.update('', {}) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + + context 'with missing required attributes' do + it 'raises RequiredAttributeError when privacy is missing' do + expect { described_class.update('test123', {}) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attributes: privacy' + ) + end + end + end + + describe '.destroy' do + context 'with valid ID' do + let(:destroyed_sharelink_data) do + { + 'access_link' => 'https://dialpad.com/recording/admincallrecording/testLinkId123456789', + 'call_id' => '1234567890123456', + 'created_by_id' => '9876543210987654', + 'date_added' => '2026-02-03T10:44:38.988809', + 'id' => 'testLinkId123456789', + 'item_id' => '5555666677778888', + 'privacy' => 'company', + 'type' => 'admincallrecording' + } + end + + it 'destroys a recording sharelink' do + stub_request(:delete, "#{base_url}/recordingsharelink/test123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 200, body: destroyed_sharelink_data.to_json, headers: { 'Content-Type' => 'application/json' }) + + sharelink = described_class.destroy('test123') + + expect(sharelink).to be_a(described_class) + expect(sharelink.id).to eq('testLinkId123456789') + expect(sharelink.access_link).to eq('https://dialpad.com/recording/admincallrecording/testLinkId123456789') + end + end + + context 'with invalid ID' do + it 'raises RequiredAttributeError when ID is nil' do + expect { described_class.destroy(nil) }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + + it 'raises RequiredAttributeError when ID is empty' do + expect { described_class.destroy('') }.to raise_error( + Dialpad::RecordingSharelink::RequiredAttributeError, + 'Missing required attribute: ID' + ) + end + end + end + end + + describe 'instance methods' do + let(:sharelink_attributes) do + { + access_link: 'https://dialpad.com/recording/admincallrecording/testLinkId123456789', + call_id: '1234567890123456', + created_by_id: '9876543210987654', + date_added: '2026-02-03T10:44:38.988809', + id: 'testLinkId123456789', + item_id: '5555666677778888', + privacy: 'company', + type: 'admincallrecording' + } + end + + let(:sharelink) { described_class.new(sharelink_attributes) } + + describe '#initialize' do + it 'sets attributes from hash' do + expect(sharelink.id).to eq('testLinkId123456789') + expect(sharelink.access_link).to eq('https://dialpad.com/recording/admincallrecording/testLinkId123456789') + expect(sharelink.call_id).to eq('1234567890123456') + expect(sharelink.created_by_id).to eq('9876543210987654') + expect(sharelink.date_added).to eq('2026-02-03T10:44:38.988809') + expect(sharelink.item_id).to eq('5555666677778888') + expect(sharelink.privacy).to eq('company') + expect(sharelink.type).to eq('admincallrecording') + end + + it 'converts string keys to symbols' do + sharelink_with_string_keys = described_class.new( + 'id' => 'testLinkId123456789', + 'access_link' => 'https://dialpad.com/recording/test', + 'privacy' => 'public' + ) + + expect(sharelink_with_string_keys.id).to eq('testLinkId123456789') + expect(sharelink_with_string_keys.access_link).to eq('https://dialpad.com/recording/test') + expect(sharelink_with_string_keys.privacy).to eq('public') + end + + it 'handles empty attributes' do + empty_sharelink = described_class.new({}) + expect(empty_sharelink.attributes).to eq({}) + end + end + + describe 'attribute access' do + it 'allows access to all defined attributes' do + expect(sharelink).to respond_to(:access_link) + expect(sharelink).to respond_to(:call_id) + expect(sharelink).to respond_to(:created_by_id) + expect(sharelink).to respond_to(:date_added) + expect(sharelink).to respond_to(:id) + expect(sharelink).to respond_to(:item_id) + expect(sharelink).to respond_to(:privacy) + expect(sharelink).to respond_to(:type) + end + + it 'raises NoMethodError for undefined attributes' do + expect { sharelink.undefined_attribute }.to raise_error(NoMethodError) + end + end + + describe 'privacy values' do + it 'handles admin privacy' do + admin_sharelink = described_class.new(sharelink_attributes.merge(privacy: 'admin')) + expect(admin_sharelink.privacy).to eq('admin') + end + + it 'handles company privacy' do + expect(sharelink.privacy).to eq('company') + end + + it 'handles owner privacy' do + owner_sharelink = described_class.new(sharelink_attributes.merge(privacy: 'owner')) + expect(owner_sharelink.privacy).to eq('owner') + end + + it 'handles public privacy' do + public_sharelink = described_class.new(sharelink_attributes.merge(privacy: 'public')) + expect(public_sharelink.privacy).to eq('public') + end + end + + describe 'type values' do + it 'handles admincallrecording type' do + expect(sharelink.type).to eq('admincallrecording') + end + + it 'handles callrecording type' do + callrecording_sharelink = described_class.new(sharelink_attributes.merge(type: 'callrecording')) + expect(callrecording_sharelink.type).to eq('callrecording') + end + + it 'handles voicemail type' do + voicemail_sharelink = described_class.new(sharelink_attributes.merge(type: 'voicemail')) + expect(voicemail_sharelink.type).to eq('voicemail') + end + end + end + + describe 'error handling' do + it 'defines RequiredAttributeError' do + expect(Dialpad::RecordingSharelink::RequiredAttributeError).to be < Dialpad::DialpadObject::RequiredAttributeError + end + end + + describe 'API integration' do + context 'when API returns error' do + it 'handles 404 errors gracefully' do + stub_request(:get, "#{base_url}/recordingsharelink/nonexistent") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 404, body: 'Not Found') + + expect { described_class.retrieve('nonexistent') }.to raise_error(Dialpad::APIError, /404 - Not Found/) + end + + it 'handles 401 errors gracefully' do + stub_request(:get, "#{base_url}/recordingsharelink/test123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 401, body: 'Unauthorized') + + expect { described_class.retrieve('test123') }.to raise_error(Dialpad::APIError, /401 - Unauthorized/) + end + + it 'handles 422 errors for create with invalid data' do + stub_request(:post, "#{base_url}/recordingsharelink") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 422, body: 'Unprocessable Entity') + + expect { described_class.create({ recording_id: 'test', recording_type: 'admincallrecording' }) }.to raise_error(Dialpad::APIError, /422 - Unprocessable Entity/) + end + + it 'handles 422 errors for update with invalid data' do + stub_request(:put, "#{base_url}/recordingsharelink/test123") + .with(headers: { 'Authorization' => "Bearer #{token}" }) + .to_return(status: 422, body: 'Unprocessable Entity') + + expect { described_class.update('test123', { privacy: 'invalid' }) }.to raise_error(Dialpad::APIError, /422 - Unprocessable Entity/) + end + end + end +end