Skip to content

Instantly share code, notes, and snippets.

@satels
Last active September 3, 2018 11:59
Show Gist options
  • Save satels/a0a0e5612588248f3659e94d75a6bf24 to your computer and use it in GitHub Desktop.
Save satels/a0a0e5612588248f3659e94d75a6bf24 to your computer and use it in GitHub Desktop.
Языки программирования: Python, Ruby. Создание звонка через API calltools.ru и получение результата на PostBack url
import requests
# Полная и актуальная документация по API: https://calltools.ru/guide_api
CALLTOOLS_PUBLIC_KEY = 'test_public_key'
CALLTOOLS_BASE_URL = 'https://calltools.ru'
CALLTOOLS_TIMEOUT = 30
class CallToolsException(Exception):
pass
def create_call(campaign_id, phonenumber, text=None, speaker='Tatyana'):
'''
Создание звонка на прозвон с генерацией ролика
:type campaign_id: int
:type phonenumber: str
:type text: str|None
:type speaker: str
:rtype: dict
'''
resp = requests.get(CALLTOOLS_BASE_URL + '/lk/cabapi_external/api/v1/phones/call/', {
'public_key': CALLTOOLS_PUBLIC_KEY,
'phone': phonenumber,
'campaign_id': campaign_id,
'text': text,
'speaker': speaker,
}, timeout=CALLTOOLS_TIMEOUT)
ret = resp.json()
if ret['status'] == 'error':
raise CallToolsException(ret['data'])
return ret
def check_status(campaign_id, phonenumber=None, call_id=None,
from_created_date=None, to_created_date=None,
from_updated_date=None, to_updated_date=None):
'''
Получение статуса звонка по номеру телефона или по call_id (ID звонка)
:type campaign_id: int
:type phonenumber: str|None
:type call_id: int|None
:type from_created_date: str|None
:type to_created_date: str|None
:type from_updated_date: str|None
:type to_updated_date: str|None
:rtype: dict
'''
if phonenumber:
url = '/lk/cabapi_external/api/v1/phones/calls_by_phone/'
elif call_id:
url = '/lk/cabapi_external/api/v1/phones/call_by_id/'
else:
raise ValueError('check_status required call_id or phonenumber')
resp = requests.get(CALLTOOLS_BASE_URL + url, {
'public_key': CALLTOOLS_PUBLIC_KEY,
'phone': phonenumber,
'call_id': call_id,
'campaign_id': campaign_id,
'from_created_date': from_created_date,
'to_created_date': to_created_date,
'from_updated_date': from_updated_date,
'to_updated_date': to_updated_date,
}, timeout=CALLTOOLS_TIMEOUT)
ret = resp.json()
if ret['status'] == 'error':
raise CallToolsException(ret['data'])
return ret
def remove_call(campaign_id, phonenumber=None, call_id=None):
'''
Удаление номера из прозвона
:type campaign_id: int
:type phonenumber: str|None
:type call_id: int|None
:rtype: dict
'''
if not phonenumber and not call_id:
raise ValueError('remove_call required call_id or phonenumber')
resp = requests.get(CALLTOOLS_BASE_URL + '/lk/cabapi_external/api/v1/phones/remove_call/', {
'public_key': CALLTOOLS_PUBLIC_KEY,
'phone': phonenumber,
'call_id': call_id,
'campaign_id': campaign_id,
}, timeout=CALLTOOLS_TIMEOUT)
ret = resp.json()
if ret['status'] == 'error':
raise CallToolsException(ret['data'])
return ret
# Статусы звонка
ATTEMPTS_EXCESS_STATUS = 'attempts_exc'
USER_CUSTOM_STATUS = 'user'
NOVALID_BUTTON_STATUS = 'novalid_button'
COMPLETE_FINISHED_STATUS = 'compl_finished'
COMPLETE_NOFINISHED_STATUS = 'compl_nofinished'
DELETED_CALL_STATUS = 'deleted'
IN_PROCESS_STATUS = 'in_process'
HUMAN_STATUSES = [
(IN_PROCESS_STATUS, 'В процессе'),
(USER_CUSTOM_STATUS, 'Пользовательский IVR'),
(ATTEMPTS_EXCESS_STATUS, 'Попытки закончились'),
(COMPLETE_NOFINISHED_STATUS, 'Некорректный ответ'),
(COMPLETE_FINISHED_STATUS, 'Закончен удачно'),
(NOVALID_BUTTON_STATUS, 'Невалидная кнопка'),
(DELETED_CALL_STATUS, 'Удалён из прозвона'),
]
# Статусы дозвона
DIAL_STATUS_WAIT = 0
DIAL_STATUS_FAILED = 1
DIAL_STATUS_HANGUP = 2
DIAL_STATUS_RING_TIMEOUT = 3
DIAL_STATUS_BUSY = 4
DIAL_STATUS_ANSWER = 5
DIAL_STATUS_ROBOT1 = 6
DIAL_STATUS_ROBOT2 = 7
DIAL_STATUS_NOVALID_BTN = 8
DIAL_STATUS_UNKNOWN = 9
DIAL_STATUS_WED = 10
DIAL_STATUS_USERSTOPLIST = 11
DIAL_STATUS_GLOBALSTOPLIST = 12
DIAL_STATUS_WED_WAIT = 13
DIAL_STATUS_ITSELF_EXC = 14
DIAL_STATUS_REMOVE = 15
DIAL_STATUSES = [
(DIAL_STATUS_WAIT, 'Ожидание вызова (звонка еще нет)'),
(DIAL_STATUS_FAILED, 'Ошибка при вызове абонента'),
(DIAL_STATUS_HANGUP, 'Абонент сбросил звонок'),
(DIAL_STATUS_RING_TIMEOUT, 'Не дозвонились'),
(DIAL_STATUS_BUSY, 'Абонент занят'),
(DIAL_STATUS_ANSWER, 'Абонент ответил'),
(DIAL_STATUS_ROBOT1, 'Ответил автоответчик'),
(DIAL_STATUS_ROBOT2, 'Ответил автоответчик'),
(DIAL_STATUS_NOVALID_BTN, 'Невалидная кнопка'),
(DIAL_STATUS_WED, 'Завершен без действия клиента'),
(DIAL_STATUS_UNKNOWN, 'Неизвестный статус'),
(DIAL_STATUS_USERSTOPLIST, 'Пользовательский стоп-лист'),
(DIAL_STATUS_GLOBALSTOPLIST, 'Глобальный стоп-лист'),
(DIAL_STATUS_WED_WAIT, 'Абонент ответил, но продолжительности разговора не достаточно для фиксации результата в статистике'),
(DIAL_STATUS_ITSELF_EXC, 'Номер абонента совпадает с CallerID'),
(DIAL_STATUS_REMOVE, 'Номер удалён из прозвона'),
]
# Примеры использование postback в django:
class PostBackForm(forms.Form):
ct_campaign_id = forms.IntegerField()
ct_phone = PhoneNumberField() # See https://github.com/stefanfoulis/django-phonenumber-field
ct_call_id = forms.IntegerField()
ct_completed = forms.DateTimeField(required=False)
ct_status = forms.CharField(required=False)
ct_dial_status = forms.IntegerField()
ct_button_num = forms.IntegerField(required=False)
ct_duration = forms.FloatField(required=False)
def calltools_postback(request):
form = PostBackForm(request.GET or None)
if form.is_valid():
CallToolsPostBackResult.objects.create(**form.cleaned_data)
return HttpResponse('OK')
# http-cookie need for Max-Age
# @see https://github.com/nahi/httpclient/issues/242#issuecomment-69020815
require 'http-cookie'
require 'httpclient'
require 'json'
require 'symbolize_keys_recursively'
# CallTools
module CallTools
# CallTools API Client
#
# @author Sergey Blohin ([email protected])
#
# @see https://calltools.ru/
# @see https://calltools.ru/guide_api/
# @see https://calltools.ru/lk/audioclips/all-speakers/
# @see https://calltools.ru/lk/pages/synth-valid-text/
# @see https://calltools.ru/lk/phones/all/
# @see https://calltools.ru/lk/users/profile/
#
# @example
# api_public_key = 'a1d7352426832e77c2a9f55e01106f1a'
# client = CallTools::Client.new api_public_key
#
# # add phone number to campaign
# # and return call_id
# campaign_id = 1_234_567_890
# phone_number = '+79185274526'
# call = client.add_call campaign_id, phone_number
# call_id = call[:call_id]
#
# # get call status by call_id
# call_result = client.call_result_by_call_id call_id
# call_status = call_result[:status]
#
# # remove phone number form campaign
# # if call status is `in_process`
# if call_status == 'is_process'
# client.remove_call_by_call_id campaign_id, phone_number
# end
class Client
# API Version
API_VERSION = 'v1'.freeze
public_constant :API_VERSION
# Base URL
BASE_URL = "https://calltools.ru/lk/cabapi_external/api/#{API_VERSION}".freeze
private_constant :BASE_URL
# HTTP STATUS OK
HTTP_STATUS_OK = 200
private_constant :HTTP_STATUS_OK
# Path Audio Upload
PATH_AUDIO_UPLOAD = '/audio/upload/'.freeze
private_constant :PATH_AUDIO_UPLOAD
# Path Def Codes By Phone
PATH_DEF_CODES_BY_PHONE = '/def_codes/by_phone/'.freeze
private_constant :PATH_DEF_CODES_BY_PHONE
# Path Phones Call
PATH_PHONES_CALL = '/phones/call/'.freeze
private_constant :PATH_PHONES_CALL
# Path Phones Call By ID
PATH_PHONES_CALL_BY_ID = '/phones/call_by_id/'.freeze
private_constant :PATH_PHONES_CALL_BY_ID
# Path Phones Calls By Phone
PATH_PHONES_CALLS_BY_PHONE = '/phones/calls_by_phone/'.freeze
private_constant :PATH_PHONES_CALLS_BY_PHONE
# Path Phones Remove Call
PATH_PHONES_REMOVE_CALL = '/phones/remove_call/'.freeze
private_constant :PATH_PHONES_REMOVE_CALL
# Path Users Balance
PATH_USERS_BALANCE = '/users/balance/'.freeze
private_constant :PATH_USERS_BALANCE
# @param [String] api_public_key
# @example
# .new(a1d7352426832e77c2a9f55e01106f1a)
def initialize(api_public_key)
@api_public_key = api_public_key
end
# Add Call To Campaign
#
# @param [Integer] campaign_id
# @param [String] phone_number phone_number in international format +79185274526
# @return [Hash] :balance, :call_id, :created, :phone
# @example
# .add_call(1234567890, '+79185274526')
def add_call(campaign_id, phone_number)
parameters = {
public_key: @api_public_key,
campaign_id: campaign_id,
phone: phone_number
}
post PATH_PHONES_CALL, parameters
end
# Add Call With Clip Generation
#
# @param [Integer] campaign_id
# @param [String] phone_number phone_number in international format +79185274526
# @param [String] text text generated by voice clip, you can also use special markup
# @param [String] speaker speaker voice, if not specified, use default from campaign settings
# @return [Hash] :call_id, :phone, :balance, :audioclip_id, :created, :callerid
# @example
# .add_call_with_clip_generation(1234567890, '+79185274526', 'Hello, Kitty', 'Tatyana')
def add_call_with_clip_generation(campaign_id, phone_number, text, speaker = nil)
parameters = {
public_key: @api_public_key,
campaign_id: campaign_id,
phone: phone_number,
text: text,
speaker: speaker
}
post PATH_PHONES_CALL, parameters
end
# Result by phone number
#
# @param [Integer] campaign_id
# @param [String] phone_number phone_number in international format +79185274526
# @param [Hash] date format is YYYY-MM-DD HH:MM:SS
# @option data [String] from_date_created
# @option data [String] from_date_created
# @option data [String] to_date_created
# @option data [String] from_date_updated
# @option data [String] to_date_updated
# @return [Array <Hash>]
# @example
# .calls_result_by_phone(1234567890, '+79185274526')
def calls_result_by_phone(campaign_id, phone_number, date = {})
parameters = {
public_key: @api_public_key,
campaign_id: campaign_id,
phone: phone_number,
from_created_date: date[:from_date_created],
to_created_date: date[:to_date_created],
from_updated_date: date[:from_date_updated],
to_updated_date: date[:to_date_updated]
}
get PATH_PHONES_CALLS_BY_PHONE, parameters
end
# Result By `call_id`
#
# @see #add_call
# @see #add_call_with_clip_generation
# @param [Integer] call_id
# @return [Hash]
# @example
# .call_result_by_call_id(1234567890)
def call_result_by_call_id(call_id)
parameters = {
public_key: @api_public_key,
call_id: call_id
}
response = get PATH_PHONES_CALL_BY_ID, parameters
response.first
end
# Remove Call By `phone_number`
#
# @param [Integer] campaign_id
# @param [String] phone_number phone_number in international format +79185274526
# @return [Hash] :call_id, :completed, :phone
# @example
# .remove_call_by_phone_number(1234567890, '+79185274526')
def remove_call_by_phone_number(campaign_id, phone_number)
parameters = {
public_key: @api_public_key,
campaign_id: campaign_id,
phone: phone_number
}
post PATH_PHONES_REMOVE_CALL, parameters
end
# Remove Call By `call_id`
#
# @see #add_call
# @see #add_call_with_clip_generation
# @param [Integer] campaign_id
# @param [Integer] call_id
# @return [Hash] :call_id, :completed, :phone
# @example
# .remove_call_by_call_id(1234567890, 1234567890)
def remove_call_by_call_id(campaign_id, call_id)
parameters = {
public_key: @api_public_key,
campaign_id: campaign_id,
call_id: call_id
}
post PATH_PHONES_REMOVE_CALL, parameters
end
# Get User Balance
#
# @param [TrueClass, FalseClass] accuracy
# @return [Integer, Float]
# @example
# .balance(true) #=> 42.666
# .balance(false) #=> 42
def balance(accuracy = false)
parameters = { public_key: @api_public_key }
response = get PATH_USERS_BALANCE, parameters
balance = response[:balance]
accuracy ? balance.to_f : balance.to_i
end
# Upload Audiofile
#
# @param [String] clip_name base filename or clip title
# @param [String] clip_filename full path to filename
# @param [String] speaker speaker name
# @param [String] text clip description
# @return [Hash] :length, :audioclip_id, :path
# @example
# .upload_audio('welcome_to_hell', '/path/to/filename.mp3', 'Maria', 'Welcome To Hell')
def upload_audio(clip_name, clip_filename, speaker = nil, text = nil)
parameters = {
public_key: @api_public_key,
clip_name: clip_name,
clip_file: File.open(clip_filename),
speaker: speaker,
text: text
}
post PATH_AUDIO_UPLOAD, parameters
end
# Definition Of The Region And Time Zone (Timezone) by `phone_number`
#
# @param [String] phone_number phone_number in international format +79185274526
# @return [Hash]
# @example
# .time_zone_by_phone_number('+79185274526')
def time_zone_by_phone_number(phone_number)
parameters = { phone: phone_number }
get PATH_DEF_CODES_BY_PHONE, parameters
end
private
# Call GET Request
#
# @param [String] path
# @param [Hash, NilClass] parameters
# @return [Hash]
# @example
# .get('/method/name/', {foo: 42, bar: 'bar'})
def get(path, parameters = nil)
request :get, path, parameters
end
# Call POST Request
#
# @param [String] path
# @param [Hash, NilClass] parameters
# @return [Hash]
# @example
# .post('/method/name/', {foo: 42, bar: 'bar'})
def post(path, parameters = nil)
request :post, path, parameters
end
# Call Request
#
# @param [Symbol] method
# @param [String] path
# @param [Hash] parameters
# @return [Hash]
# @example
# .request(:get, '/method/name/', {foo: 42, bar: 'bar'})
def request(method, path, parameters)
url = "#{BASE_URL}#{path}"
case method
when :get
response_validator HTTPClient.get url, parameters
when :post
response_validator HTTPClient.post url, parameters
else
raise method.to_s
end
end
# Check http.code is HTTP_STATUS_OK and http.body is JSON
#
# @param [HTTP::Message] response
# @return [Hash] http.body as JSON
# @example
# .response_validator(HTTPClient.get('http://httpbin.org/get'))
def response_validator(response)
status = response.status
source_body = response.body
# raise code if http.code is not HTTP_STATUS_OK
raise status.to_s unless status == HTTP_STATUS_OK
# raise source http.body if http.body is not JSON
begin
body = JSON.parse source_body
# { 'foo' => 42 } => { foo: 42 }
body.symbolize_keys_recursively!
rescue JSON::ParserError
raise source_body
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment