Порой реклама курсов застаёт врасплох.
Обычно я всё узнаю из открытых источников и документаций, но при виде объявления "Купите курс "Как продать курс" даже мне сложно устоять и не подписаться на бесплатный вебинар чудодейственной методики. И вот буквально через минуту я уже мысленно рисую логотип своего SkillPropil... Сегодня про одну из популярных тем курсовых – API.
Содержание
Что такое API
API - способ программного взаимодействия с приложениями. Благодаря API ваш код может получить что-то от программы для своих нужд или воспользоваться её помощью в решении собственной задачи, а потом даже не поздравить с днем рождения. Как и всё гениальное, человек скопировал эту идею у природы, поэтому сперва обратимся к первоисточнику:
- Слышь, сюда иди. Дай айфон позвонить.
Разберём подробно, что в данном случае делает ваш более опытный коллега:
- Сперва нужно указать, куда мы обращаемся, для этого используется команда
Слышь
- Ваш метод авторизации ему неизвестен, поэтому он не добавляет имя, а просто использует дефолтные настройки типа anonymous
- Чтобы эффективно с вами взаимодействовать, специалисту требуется узнать перечень доступных методов, для этого он просит подойти поближе, вызывая стандартную для вашего класса программ команду
иди
с параметром сюда - И наконец, подробно изучив библиотеку, коллега решает вызвать метод
дай
с обязательным параметром айфон и опциальным позвонить
Конечно есть примеры и попроще. Если вы часто работаете с Google Tag Manager, то наверняка не раз общались c DOM через его API как-нибудь вот так:
document.querySelector(
'#gatsby-focus-wrapper > div > div > div.fixed.z-10.bottom-0.left-0.w-screen.h-screen.bg-white.overflow-y-scroll.flex.flex-col-reverse > nav > div:nth-child(2) > a:nth-child(3)'
)
Однако сегодня мы всё-таки остановимся на чем-то не таком избитом, и разберём способ авторизации Oauth2, используемый Яндекс и Google, но на примере более популярного сервиса - Admitad, а бонусом сделаем Telegram бота для регулярной автоматической отчетности.
Принцип авторизации OAuth2
OAuth2 - способ авторизации, при котором доступ предоставляется третьей стороне.
Таблица полигамных отношений
Партнер | Обязанности |
---|---|
Сервис | Admitad, предоставляющий методы API |
Пользователь | Аккаунт в Сервисе, на котором находятся необходимые данные |
Приложение | Код Разработчика, использующий API Сервиса для доступа к данным Аккаунта |
Сам коитус обычно происходит следующим образом:
- Разработчик 👶 регистрирует своё Приложение в Сервисе и получает для него всякие там аналоги СНИЛС и ИНН: как минимум идентификатор Приложения client_id, бывают еще адрес доставки redirect_uri, пароли и секретики, куда же без них 😊
- Почуяв слабину, Приложение запрашивает у Сервиса доступ к данным Пользователя, показывая ксиву с прошлого шага, а также обозначив, куда именно будет проникать и что себе там будет позволять: scope и grant_type
- Сервис предупреждает, что оплата почасовая, возвращает Приложению временный пропуск access_token со сроком действия expires_in, а так же карточку постоянного покупателя refresh_token
- Когда природа снова позовёт, Приложение может уже самостоятельно обновить свой временный пропуск access_token с помощью выданного refresh_token, потому что в конце концов все же свои 😜
Общий принцип можно еще почитать по ссылке 18+
https://tools.ietf.org/html/draft-ietf-oauth-v2-16
Подключение к Admitad API
У адмиташи есть упрощенный вариант описанной выше процедуры для тех случаев, когда по сути партнеров двое, то есть как раз для нас с вами, называется он Клиентская авторизация, и по этой ссылке все уже достаточно подробно написано, так что довольно прелюдий, пришло время кодить.
Ключи можно получить в разделе для разработчиков, если у вас нормальный аккаунт, не гостевой, иначе просите у владельца.
Пример на каком-то там питоне 🐍
# копипаст из доки для Клиентской авторизации
from base64 import b64encode
# client_id и client_secret со страницы https://developers.admitad.com/
client_id='413a534538df8f2ckjwia8d7725c23b'
client_secret='2d3eab98386dd8wkwe84j7e2a71b6b'
data = client_id + ':' + client_secret
data_b64_encoded = b64encode(data)
# запрос на получение токенов
import requests
url = 'https://api.admitad.com/token/?grant_type=client_credentials&client_id=' + client_id + '&scope=statistics'
headers = {'Authorization': 'Basic ' + data_b64_encoded}
authInfo = requests.post(url, headers=headers)
print(authInfo.text)
// все слишком очевидно
const https = require('https')
const [client_id, client_secret] = [
'413a534538df8f2ckjwia8d7725c23b',
'2d3eab98386dd8wkwe84j7e2a71b6b',
]
const options = {
method: 'POST',
hostname: 'api.admitad.com',
path:
'/token/?grant_type=client_credentials&client_id=' +
client_id +
'&scope=statistics',
headers: {
Authorization:
'Basic ' +
new Buffer.from(client_id + ':' + client_secret).toString('base64'),
},
}
const request = https.request(options, (response) => {
response.setEncoding('utf8')
response.on('data', function (chunk) {
console.log(chunk)
})
})
request.write('')
request.end()
Если всё прокатило, то адмитад вернёт строку с json, в которой нас в первую очередь интересует поле access_token, а чтобы узаконить отношения, понадобятся еще refresh_token и expires_in.
Скачивание данных из Admitad
Наконец, можно получить сами данные. Пока сильно наглеть не будем, десяток строчек.
Снова пример на никому не нужном 🐍
import json
# access_token из данных с прошлого шага
accessToken = json.loads(authInfo.text)['access_token']
# даты и лимит
url = 'https://api.admitad.com/statistics/actions/?date_start=01.12.2019&date_end=05.12.2019&limit=10'
headers = {'Authorization': 'Bearer ' + accessToken}
# согласно доке get вместо post
data = requests.get(url, headers=headers)
print(data.text)
// да, node.js
const https = require('https')
const accessToken = '07d0787r1985c49958eb'
const options = {
method: 'GET',
hostname: 'api.admitad.com',
path: '/statistics/actions/?date_start=01.12.2019&date_end=05.12.2019&limit=10',
headers: {
Authorization: 'Bearer ' + accessToken,
},
}
const req = https.request(options, (res) => {
res.setEncoding('utf8')
res.on('data', function (chunk) {
console.log(chunk)
})
req.on('error', (error) => {
console.error(error)
})
})
req.write('')
req.end()
Поздравляю, теперь вы клуба член, и вам открыт удивительный мир апишек, и да, можно всех посмотреть.
Работа с Admitad в Apps Script
Переходим к неофициальной части, где разберемся, как это всё получать регулярно в лучшем виде и месте. Автоматизировать буду на Apps Script, потому что это бесплатно и похоже на Javascript 😍, так что никаких 🐍 больше.
Готовый скрипт можно скачать в моём репозитории, там же и коротенькая инструкция по деплою.
Итак, дайте жизнь новому проекту на https://script.google.com, далее пойдут листинги с комментами.
И создал я в первый день файл с конфигом config.gs и занёс в него выданные Адмитадом credentials
const config = {
clientId: '413a534538df8f2ckjwia8d7725c23b',
clientSecret: '2d3eab98386dd8wkwe84j7e2a71b6b',
}
Теперь набросаем общую функцию для работы с самим сервисом, и её начальный метод – авторизацию, который, если всё пройдёт хорошо, понадобится только один раз, самый первый.
function Admitad(config) {
function authorize(callback) {
// собираем запрос
var url =
'https://api.admitad.com/token/?grant_type=client_credentials&client_id=' +
config.clientId +
'&scope=statistics'
var options = {
method: 'post',
headers: {
Authorization:
'Basic ' +
Utilities.base64Encode(config.clientId + ':' + config.clientSecret),
},
}
// для отладки,[] позволяет вывести несколько значений, посмотреть по Ctrl(Cmd) + Enter
Logger.log(['=== AUTH REQUEST', url, options])
// отправляем запрос
var response = UrlFetchApp.fetch(url, options).getContentText()
var json = JSON.parse(response)
Logger.log(['=== AUTH RESPONSE', json])
// проверяем ответ
if (json) {
// сохраняем все токены
config.expirationTime = Date.now() + parseInt(json.expires_in) * 1000
config.accessToken = json.access_token
config.refreshToken = json.refresh_token
} else {
Logger.log(['Ошибка авторизации', response])
}
// callback нужен, чтобы последовательно выполнить еще действие
return callback()
}
}
accessToken, refreshToken и expirationDate периодически будут обновляться, поэтому их временные значения надо куда-то сохранять. Я нагуглил в Apps Script по этому поводу глобальный объект Properties service, соответственно, в конфиг добавил геттеры и сеттеры, и вроде это удобно, хоть и не выглядит так 🤣
var config = {
// ...
get accessToken() {
if(!this._accessToken) {
var scriptProperties = PropertiesService.getScriptProperties()
this._accessToken = scriptProperties.getProperty('admitad.accessToken')
}
return this._accessToken
},
set accessToken(value) {
var scriptProperties = PropertiesService.getScriptProperties()
scriptProperties.setProperty('admitad.accessToken', value)
this._accessToken = value
},
get refreshToken() {
if(!this._refreshToken) {
var scriptProperties = PropertiesService.getScriptProperties()
this._refreshToken = scriptProperties.getProperty('admitad.refreshToken')
}
return this._refreshToken
},
set refreshToken(value) {
var scriptProperties = PropertiesService.getScriptProperties()
scriptProperties.setProperty('admitad.refreshToken', value)
this._refreshToken = value
},
get expirationTime() {
if (!this._expirationTime) {
var scriptProperties = PropertiesService.getScriptProperties()
this._expirationTime = scriptProperties.getProperty('admitad.expirationTime')
}
return this._expirationTime
},
set expirationTime(value) {
var scriptProperties = PropertiesService.getScriptProperties()
scriptProperties.setProperty('admitad.expirationTime', value)
this._expirationTime = value
},
}
Теперь нужно то, чем мы будем эти токены обновлять. Метод похож на предыдущий, только учитываем вариант, когда refreshToken отсутствует.
function Admitad(config) {
// ...
function refreshToken(callback) {
if (config.refreshToken) {
var url = 'https://api.admitad.com/token/'
var options = {
method: 'post',
payload:
'grant_type=refresh_token' +
'&client_id=' +
config.clientId +
'&refresh_token=' +
config.refreshToken +
'&client_secret=' +
config.clientSecret,
}
Logger.log(['=== REFRESH REQUEST', url, options])
var response = UrlFetchApp.fetch(url, options).getContentText()
var json = JSON.parse(response)
Logger.log(['=== REFRESH RESPONSE', json])
if (json) {
config.expirationTime = Date.now() + parseInt(json.expires_in) * 1000
config.accessToken = json.access_token
config.refreshToken = json.refresh_token
} else {
Logger.log(['Ошибка обновления токена', response])
}
return callback()
} else {
// если refreshToken нет, вызываем обычную авторизацию
return authorize(callback)
}
}
}
Последний метод Адмитада получает данные по транзакциям. Здесь немножко tricky, потому что количество строк за раз ограничено 500. Чтобы получить остальные, в запросе используется параметр offset со стартовой точкой.
Я беру совершенные транзакции за период, но можно достать, например, обновления статусов за период и другое.
function Admitad(config) {
// ...
// начало и конец периода, накопленные данные
this.actions = function(first, last, data) {
// если данных еще нет
data = data || []
// вспомогательная функция для проверки токена
var actionsHelper = function() {
var url =
'https://api.admitad.com/statistics/actions/?date_start=' +
first +
'&date_end=' +
last +
'&limit=500&offset=' +
data.length
var options = {
method: 'get',
headers: {
Authorization: 'Bearer ' + config.accessToken,
},
}
var response = UrlFetchApp.fetch(url, options).getContentText()
var json = JSON.parse(response)
if (json) {
var results = json.results
// аккумулируем полученные данные
data = data.concat(results)
Logger.log(['=== TOTAL', json._meta.count, ' CURRENT ', data.length])
// json._meta.count содержит общее количество строк по запросу
// проверяем остатки и рекурсивно вызываем при необходимости, отдавая накопленные данные
return json._meta.count > data.length
? this.actions(first, last, data)
: data
} else {
Logger.log(['Ошибка получения actions', response])
}
return data
// привязываем контекст this, чтобы можно было выполнить this.actions()
}.bind(this)
// обновляем протухший accessToken
return (config.expirationTime && config.expirationTime <= Date.now()) ||
!config.accessToken
? refreshToken(actionsHelper)
: actionsHelper()
}
}
Агрегация данных
Пришло время возрадоваться всем любителям 🐍, наконец будут знакомые слова, потому что функции для работы с данными я засунул в файл Dataframe.gs, на этом всё.
Обязательно нужен счетчик транзакций, который возьмёт уникальные значения order_id. Замечу, что, например, у АлиЭкспресса (на момент написания поста) каждая позиция - отдельный order_id, то есть купил там пользователь оптом десяток одинаковых безделушек – вы увидите 10 транзакций. Для таких случаев имеет смысл считать конкатенации какого-нибудь идентификатора пользователя из subid и времени покупки action_date, но сейчас попроще.
function Dataframe(data) {
this.transactions = function(status) {
// по умолчанию считаем все транзакции, а можно передать и approved|pending
status = status || '.*'
// вспомогательная функция для подсчета уников
function countDistinct(arr) {
var counts = {}
for (var i = 0; i < arr.length; i++) {
counts[arr[i]] = 1 + (counts[arr[i]] || 0)
}
return Object.keys(counts).length
}
var orders = data
// фильтруем только нужные статусы
.filter(function(order) {
return order.status.match(status)
})
.map(function(t) {
// достаём номера заказов
return t.order_id
})
return countDistinct(orders)
}
}
Ну и куда же без 💰, их тоже посчитаем.
function Dataframe(data) {
// ...
this.revenue = function(status) {
status = status || '.*'
return data
.filter(function(order) {
return order.status.match(status)
})
.reduce(function(s, c) {
var t = c.payment * config[c.currency]
return s + t
}, 0)
}
}
Кто внимательный – увидел валютку, её надобно добавить в конфиг, а можно ещё доставать из гугл таблички, чтобы не пришлось каждый раз выкатывать обновления курса, или вообще запрашивать из какого-нибудь сервиса.
var config = {
clientId: '413a534538df8f2ckjwia8d7725c23b',
clientSecret: '2d3eab98386dd8wkwe84j7e2a71b6b',
+ 'USD': 66,
+ 'EUR': 75,
+ 'RUB': 1,
// ...
Отправка отчетов в Telegram
Отчет бесполезен, если его никто не смотрит, а как известно, путь к сердцу миллениала лежит через мессенджеры. Я уже писал ранее, как создать бота в телеге. Возьмите оттуда токен, идентификатор чата и добавьте в конфиг, он всё стерпит.
var config = {
clientId: '413a534538df8f2ckjwia8d7725c23b',
clientSecret: '2d3eab98386dd8wkwe84j7e2a71b6b',
'USD': 66,
'EUR': 75,
'RUB': 1,
+ botToken: '862713267:AAHavQqWrRTsktlDMeU5SGFgorkwkcwWRtYc',
+ groupId: '289798293',
// ...
Небольшая функция для самой отправки.
function telegram(message) {
UrlFetchApp.fetch(
'https://api.telegram.org/bot' + config.botToken + '/sendMessage',
{
method: 'post',
payload: {
chat_id: config.groupId,
parse_mode: 'Markdown',
text: message,
},
}
)
}
Автоматизация отчетности
Пришло время всё это склеить и поставить на таймер.
function Oleg() {
// помощницы для красивостей
function r(num) {
return Math.floor(num)
}
function p(num) {
return ' (' + Math.floor(num * 100) + '%)'
}
var data = []
var admitad = new Admitad(config)
// достаем последние 7 дней
var yesterday = new Date(new Date().setDate(new Date().getDate() - 1))
var _8days_ago = new Date(new Date().setDate(new Date().getDate() - 8))
// Utilities - приблуда Apps Script с полезными штуками
data = admitad.actions(
Utilities.formatDate(_8days_ago, 'GMT+3', 'dd.MM.yyyy'),
Utilities.formatDate(yesterday, 'GMT+3', 'dd.MM.yyyy')
)
var df1 = new Dataframe(data)
var tr1 = df1.transactions()
var re1 = df1.revenue()
// предыдущие 7 дней для сравнения
var _9days_ago = new Date(new Date().setDate(new Date().getDate() - 9))
var _16days_ago = new Date(new Date().setDate(new Date().getDate() - 16))
data = admitad.actions(
Utilities.formatDate(_16days_ago, 'GMT+3', 'dd.MM.yyyy'),
Utilities.formatDate(_9days_ago, 'GMT+3', 'dd.MM.yyyy')
)
var df2 = new Dataframe(data)
var tr2 = df2.transactions()
var re2 = df2.revenue()
// собираем соообщение
var message =
'\n*Last 7, total*' +
'\n' +
'\n*Orders*: ' +
tr1 +
p(tr1 / tr2 - 1) +
'\n*Revenue*: ' +
r(re1) +
p(re1 / re2 - 1)
// отправляем сообщение
telegram(message)
}
Чтоб потестить, сделайте Run функции Oleg()
.
На вкладке Edit есть ссылка на триггеры проекта, идём по ней и жмякаем Add Trigger справа внизу.
Вот пример настроек
Ограничения
На выполнение скрипта GAS выделяет 6 минут, так что отчет за год так качать не стоит, а за вчера, текущий месяц и MoM вместе вполне себе можно. Иногда Admitad не отвечает 😤, но бывает это нечасто.
В целом я успешно использовал похожий скрипт на одном крупном проекте, а специально для статьи его еще и переписал. Надеюсь, дочитавшим до конца тоже пригодится.