Полезные массовые действия с API Octo Browser

Полезные массовые действия с API Octo Browser
Alex Phillips
Alex Phillips

Customer Service Specialist

API (Application Programming Interface) — это интерфейс, который позволяет программам взаимодействовать друг с другом. В случае Octo Browser API позволяет отправлять HTTP-запросы к серверу Octo и автоматизировать управление браузерными профилями. 

В прошлой статье мы подробно описали, что такое API, рассказали про способы запуска скриптов и объяснили структуру построения запроса. Теперь же разберем несколько полезных примеров, наиболее востребованных у наших пользователей. Прочитав эту статью, вы научитесь получать и сохранять имена профилей, массово добавлять в созданные профили расширения, стартовые страницы и закладки, а также выгружать данные профилей в таблицу. Ниже — подробные инструкции и готовый код.

Содержание

Подготовка к работе

  1. Скачайте и установите VS Code.

  2. Скачайте и установите node.js.

  3. Создайте папку в удобном для вас месте и назовите ее, к примеру, octo_api.

  4. Откройте эту папку в VS Code.

  5. Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например, get_titles.js.

  6. Откройте терминал и выполните команду npm install axios, чтобы установить зависимость для NodeJS. 

  7. Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned и подтвердите. Затем повторите предыдущий пункт.

  8. Запустите клиент Octo Browser.

Где найти API-токен

Чтобы взаимодействовать с клиентом Octo Browser через API, вам потребуется API-токен. Он доступен пользователям с подпиской Base и выше. API-токен отображается в клиенте Octo Browser в настройках мастер-аккаунта на вкладке «Дополнительные» (другие члены команды API-токен не видят).

API-токен Octo Browser

Ограничения API (Rate Limits)

Стоит учитывать, что API Octo Browser имеет ограничения на количество запросов. В приведенных сниппетах используется функция check_limits, которая проверяет заголовки API-лимитов и автоматически делает паузу, если осталось мало запросов. Это помогает избежать ошибок 429 Too Many Requests.

При запросах к Public API тратится 1 RPM и 1 RPH за один запрос. При запросах к Local API RPM/RPH не снимаются, кроме запросов POST Start Profile (1 RPM и 1 RPH) и POST One-time profile (4 RPM и 4 RPH).

Точное число профилей, которые вы можете запустить/cоздать/удалить на определенной подписке, полностью зависит от ваших скриптов и их логики. Вы можете самостоятельно рассчитать необходимое количество запросов для вашей схемы работы, исходя из информации о том, какие запросы влияют на количество RPH и RPM.

Некоторые API-запросы могут требовать более сложной обработки, а значит, их выполнение может стоить больше, чем один запрос из лимитов. Это необходимо для балансировки нагрузки на сервер и оптимальной производительности для всех пользователей.

RPM (Requests Per Minute) — запросы в минуту.
RPH (Requests Per Hour) — запросы в час.
Значения лимитов зависят от вашей подписки.

С подготовкой разобрались, переходим к полезным сниппетам.

Получение имен профилей и сохранение их в .txt-файл

Этот сниппет будет полезен, когда нужно быстро собрать список всех созданных профилей в одном месте. Помогает упростить учет профилей, проверку их фактического наличия и дальнейшую работу с ними.

  1. В папке octo_api создайте файл get_titles.js в octo_api и скопируйте в него код сниппета. 

  2. В поле octo_token вместо OCTO_API_TOKEN вставьте свой API-токен из клиента Octo Browser.

OCTO_API_TOKEN
  1. Сохраните изменения в файле.

  2. Введите в терминал команду node get_titles.js и запустите скрипт. 

get_titles.js

Названия профилей будут сохранены построчно в файл profiles_titles.txt в папке, где находится скрипт. 

Сниппет для сохранения имен профилей в .txt-файл

const fs = require('fs').promises;
const axios = require('axios');
//This is the configuration. Specify your Octo API Token here
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url, 
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_titles(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function write_file(profiles) {
    const titles = profiles.map(item => item.title).filter(Boolean);
    try {
        await fs.writeFile('./profiles_titles.txt', titles.join('\n'), 'utf-8');
    } catch (error) {
        console.error(`ERROR: While trying writing the file some error occured:\n${error}`);
    }
}
(async () => {
    const total_pages = await get_total_pages();
    const profiles = await get_all_profiles_titles(total_pages);
    await write_file(profiles);
    console.log('Finished. Check profiles_titles.txt file...');
})()
const fs = require('fs').promises;
const axios = require('axios');
//This is the configuration. Specify your Octo API Token here
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url, 
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_titles(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function write_file(profiles) {
    const titles = profiles.map(item => item.title).filter(Boolean);
    try {
        await fs.writeFile('./profiles_titles.txt', titles.join('\n'), 'utf-8');
    } catch (error) {
        console.error(`ERROR: While trying writing the file some error occured:\n${error}`);
    }
}
(async () => {
    const total_pages = await get_total_pages();
    const profiles = await get_all_profiles_titles(total_pages);
    await write_file(profiles);
    console.log('Finished. Check profiles_titles.txt file...');
})()

Добавление расширений / стартовых страниц / закладок в профили

С помощью этой автоматизации вы сможете быстро привести конфигурацию профилей к единому виду и не добавлять одинаковые параметры вручную. 

Для добавления расширений вам потребуются их UUID:

  1. Откройте профиль с подключенным расширением.

  2. В адресной строке профиля введите chrome://extensions/ и нажмите Enter.

  3. На нужном расширении нажмите кнопку «Сведения» (Details).

chrome://extensions/
  1. Внизу страницы скопируйте UUID расширения вместе с версией.

UUID расширения

Cпособы для получения UUID с помощью API вы можете найти в документации.

Затем вам нужно отредактировать код:

  1. Создайте файл adding_info.js в папке octo_api.

  2. Скопируйте в него сниппет.

  3. В поле octo_token вставьте API-токен.

  4. В поле extensions (расширения) добавьте UUID необходимых расширений.

  5. В поле bookmarks (закладки) добавьте name — название закладки, URL — адрес сайта.

  6. В поле start_pages (стартовые страницы) добавьте URL сайтов, которые хотите открывать автоматически при запуске профиля. 

Если вам необходимо добавить не все, а только extensions/bookmarks/start_pages, — вы можете удалить ненужные вам параметры из сниппета:

data: {

        extensions: ["nkbihfbeogaeaoehlefnkodbefgpgknn@12.17.2"], //удаляем, если не нужно добавлять расширения

        bookmarks: [

            { name: "google", url: "https://google.com" },

            { name: "facebook", url: "https://facebook.com" }

        ],//удаляем, если не нужны закладки 

        start_pages: ["https://google.com", "https://facebook.com"] //удаляем, если не нужны стартовые страницы

    }
data: {

        extensions: ["nkbihfbeogaeaoehlefnkodbefgpgknn@12.17.2"], //удаляем, если не нужно добавлять расширения

        bookmarks: [

            { name: "google", url: "https://google.com" },

            { name: "facebook", url: "https://facebook.com" }

        ],//удаляем, если не нужны закладки 

        start_pages: ["https://google.com", "https://facebook.com"] //удаляем, если не нужны стартовые страницы

    }
 adding_info.js
  1. Сохраните файл adding_info.js.

  2. Запустите файл с помощью команды node adding_info.js.

Нужные расширения, закладки и стартовые страницы будут добавлены во все ваши профили. Если изменения требуются для отдельных профилей — смотрите пример в документации.

adding_info.js

Перед запуском скрипта убедитесь, что профили, в которые необходимо добавить данные, не запущены. Запущенные профили обновлены не будут. 

Сниппет для добавления расширений, стартовых страниц и закладок в профили

const axios = require('axios');
// This is the configuration. Specify your Octo API Token here; you can modify this for your personal needs
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
    data: {
        "extensions": ["aapfglkgnhmiaabhiijkjpfmhllaodgp@4.2.3"],
        "bookmarks": [
            { "name": "google", "url": "https://google.com" },
            { "name": "facebook", "url": "https://facebook.com" }
        ],
        "start_pages": ["https://google.com", "https://facebook.com"]
    }
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
//functions
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_profiles() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_uuids(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function patch_all_profiles(profiles) {
    let updated = [];
    let not_updated = [];
    for (let profile of profiles) {
        try {
            const response = await OCTO_REMOTE_API.patch(`/profiles/${profile.uuid}`, config.data);
            await check_limits(response);
            updated.push(profile);
            console.log(`Successfully updated ${profile.uuid}`);
        } catch (error) {
            not_updated.push(profile);
            console.error(`ERROR: Can't patch profile ${profile.uuid}`);
        }
    }
    return [updated, not_updated];
}
(async () => {
    const total = await get_total_profiles();
    const profiles = await get_all_profiles_uuids(total);
    const [updated, not_updated] = await patch_all_profiles(profiles);
    console.log(`Finished process:\nUpdated: ${updated.length}\nNot updated: ${not_updated.length}`);
})();
const axios = require('axios');
// This is the configuration. Specify your Octo API Token here; you can modify this for your personal needs
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
    data: {
        "extensions": ["aapfglkgnhmiaabhiijkjpfmhllaodgp@4.2.3"],
        "bookmarks": [
            { "name": "google", "url": "https://google.com" },
            { "name": "facebook", "url": "https://facebook.com" }
        ],
        "start_pages": ["https://google.com", "https://facebook.com"]
    }
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
//functions
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_profiles() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_uuids(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function patch_all_profiles(profiles) {
    let updated = [];
    let not_updated = [];
    for (let profile of profiles) {
        try {
            const response = await OCTO_REMOTE_API.patch(`/profiles/${profile.uuid}`, config.data);
            await check_limits(response);
            updated.push(profile);
            console.log(`Successfully updated ${profile.uuid}`);
        } catch (error) {
            not_updated.push(profile);
            console.error(`ERROR: Can't patch profile ${profile.uuid}`);
        }
    }
    return [updated, not_updated];
}
(async () => {
    const total = await get_total_profiles();
    const profiles = await get_all_profiles_uuids(total);
    const [updated, not_updated] = await patch_all_profiles(profiles);
    console.log(`Finished process:\nUpdated: ${updated.length}\nNot updated: ${not_updated.length}`);
})();

Выгрузка данных профилей в Google Sheets

Данные ваших профилей можно выгрузить в таблицу. Так удобнее структурировать информацию при работе, сортировать и анализировать данные. Можно выгрузить в следующие параметры профиля: 

  • UUID,

  • теги,

  • имя,

  • описание.

  1. Создайте файл google_sheets.js в папке octo_api.

  2. Добавьте код сниппета в файл.

  3. Заполните поле octo_token — API-токен можно найти в клиенте Octo.

  4. Заполните поле table_id — это ID таблицы Google Sheets.

  5. Заполните поле table_sheet_name — это название листа внутри таблицы Google Sheets, куда вам нужна выгрузка.

google_sheets.js
  1. Сохраните изменения.

  2. Добавьте в папку octo_api файлы credentials.json и token.json.

  3. Запустите скрипт через терминал VS Code командой node google_sheets.js.

  4. После выполнения скрипта данные будут загружены в указанную таблицу.

    google_sheets.js

Сниппет для выгрузки данных профилей в Google Sheets

const axios = require('axios');
const { google } = require('googleapis');
const token = require('./token.json');
const credentials = require('./credentials.json');


const oAuth2Client = new google.auth.OAuth2(
    credentials.installed.client_id,
    credentials.installed.client_secret,
    credentials.installed.redirect_uris[0]
);


oAuth2Client.setCredentials(token);


//configs
//here is config. Paste your Octo API Token, table id and sheet name here
const config = {
    octo_token: "OCTO_API_TOKEN",
    table_id: "1pqnVysHymZSI5Lzx6odasdasAqE022fqUEf6q5LnOqAhylLSM",
    table_sheet_name: "Profiles",
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}


const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});


//functions
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}


async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}




async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}


async function get_all_profiles_data(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title,description,tags`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}


async function fill_sheet_with_data(oAuth2Client, spreadsheet_id, list_name, data) {
    const sheets = google.sheets({ version: 'v4', auth: oAuth2Client });


    try {
        const sheetInfo = await sheets.spreadsheets.get({
            spreadsheetId: spreadsheet_id,
        });


        const sheetExists = sheetInfo.data.sheets.some(
            sheet => sheet.properties.title === list_name
        );


        if (!sheetExists) {
            await sheets.spreadsheets.batchUpdate({
                spreadsheetId: spreadsheet_id,
                resource: {
                    requests: [{
                        addSheet: {
                            properties: {
                                title: list_name
                            }
                        }
                    }]
                }
            });
            console.log(`Создан новый лист "${list_name}".`);
        }


        const headers = ['Uuid', 'Tags', 'Title', 'Description'];
        const values = data.map(item => [
            item.uuid,
            Array.isArray(item.tags) ? item.tags.join(', ') : item.tags,
            item.title,
            item.description || ''
        ]);


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A1:D1`,
            valueInputOption: 'RAW',
            resource: {
                values: [headers]
            }
        });


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A2:D${values.length + 1}`,
            valueInputOption: 'RAW',
            resource: {
                values: values
            }
        });


        console.log(`Данные успешно записаны в лист "${list_name}".`);


    } catch (err) {
        console.error('Ошибка при обновлении таблицы:', err);
        throw err;
    }
}


(async () => {
    const total_pages = await get_total_pages();
    const profiles_data = await get_all_profiles_data(total_pages);
    await fill_sheet_with_data(oAuth2Client, config.table_id, config.table_sheet_name, profiles_data);
})()

const axios = require('axios');
const { google } = require('googleapis');
const token = require('./token.json');
const credentials = require('./credentials.json');


const oAuth2Client = new google.auth.OAuth2(
    credentials.installed.client_id,
    credentials.installed.client_secret,
    credentials.installed.redirect_uris[0]
);


oAuth2Client.setCredentials(token);


//configs
//here is config. Paste your Octo API Token, table id and sheet name here
const config = {
    octo_token: "OCTO_API_TOKEN",
    table_id: "1pqnVysHymZSI5Lzx6odasdasAqE022fqUEf6q5LnOqAhylLSM",
    table_sheet_name: "Profiles",
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}


const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});


//functions
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}


async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}




async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}


async function get_all_profiles_data(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title,description,tags`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}


async function fill_sheet_with_data(oAuth2Client, spreadsheet_id, list_name, data) {
    const sheets = google.sheets({ version: 'v4', auth: oAuth2Client });


    try {
        const sheetInfo = await sheets.spreadsheets.get({
            spreadsheetId: spreadsheet_id,
        });


        const sheetExists = sheetInfo.data.sheets.some(
            sheet => sheet.properties.title === list_name
        );


        if (!sheetExists) {
            await sheets.spreadsheets.batchUpdate({
                spreadsheetId: spreadsheet_id,
                resource: {
                    requests: [{
                        addSheet: {
                            properties: {
                                title: list_name
                            }
                        }
                    }]
                }
            });
            console.log(`Создан новый лист "${list_name}".`);
        }


        const headers = ['Uuid', 'Tags', 'Title', 'Description'];
        const values = data.map(item => [
            item.uuid,
            Array.isArray(item.tags) ? item.tags.join(', ') : item.tags,
            item.title,
            item.description || ''
        ]);


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A1:D1`,
            valueInputOption: 'RAW',
            resource: {
                values: [headers]
            }
        });


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A2:D${values.length + 1}`,
            valueInputOption: 'RAW',
            resource: {
                values: values
            }
        });


        console.log(`Данные успешно записаны в лист "${list_name}".`);


    } catch (err) {
        console.error('Ошибка при обновлении таблицы:', err);
        throw err;
    }
}


(async () => {
    const total_pages = await get_total_pages();
    const profiles_data = await get_all_profiles_data(total_pages);
    await fill_sheet_with_data(oAuth2Client, config.table_id, config.table_sheet_name, profiles_data);
})()

Как получить table_id

  1. Откройте или создайте таблицу в Google Sheets.

  2. Скопируйте ID таблицы — это часть в URL между /d/ и /edit.

Например, если ссылка: https://docs.google.com/spreadsheets/d/1GoPnhkStxFFxDzGZjjXSyjT_H_msHlSDx51tBROewOA/edit, то table_id: 1GoPnhkStxFFxDzGZjjXSyjT_H_msHlSDx51tBROewOA.

Как получить файл credentials.json

  1. Авторизуйтесь в персональный (не состоящий в организации) Gmail-аккаунт.

  2. Перейдите по ссылке https://console.cloud.google.com/.

  3. Выберите из списка любую европейскую страну и нажмите Agree and continue.

  4. Кликните Select a project, далее New project.

  5. Введите любое название проекта и нажмите Create.

  6. Перейдите по ссылке https://console.cloud.google.com/auth. Кликните Get Started.

  7. Укажите любое App Name, вашу почту и нажмите Next.

  8. Выберите External, еще раз укажите свою почту, кликните Next, активируйте чекбокс с согласием, затем нажмите Continue и Finish.

  9. Перейдите по ссылке https://console.cloud.google.com/auth/audience.

  10. В разделе Test Users нажмите кнопку Add Users.

Create OAuth Client
  1. . Добавьте вашу почту и нажмите Save.

  2. . На странице https://console.cloud.google.com/auth/overview нажмите Create OAuth Client.

Create OAuth Client
  1. . Выберите из списка Desktop app, введите любое название и кликните на Create.

  2. . Появится всплывающее окно, из которого нужно скачать JSON-файл.

Create OAuth Client
  1. . Перейдите по ссылке https://console.cloud.google.com/apis/library.

  2. . В строку поиска впишите Google Sheets API.

  3. . Выберите найденный API, затем нажмите Enable.

  4. . Откройте ранее скачанный JSON-файл.

  5. . Измените значение параметра redirect_uris с http://localhost на "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"]. 

Делайте замену аккуратно, чтобы не стереть скобки.

credentials.json
  1. . Сохраните файл.

  2. . Переименуйте файл в credentials.json.

  3. . Добавьте его в папку octo_api.

Как получить файл token.json

  1. Создайте в папке octo_api файл get_token.js и откройте в VS Code.

  2. Введите в терминал VS Code команду Npm install googleapis.

  3. Вставьте в файл get_token.js скрипт для генерации токена и сохраните.

  4. Запустите скрипт командой node get_token.js.

  5. Вы увидите ссылку в виде Open this URL in browser: https://accounts.google.com/o/oauth2/…

  6. Откройте ссылку.

  7. Войдите в свой аккаунт Google.

  8. Нажмите кнопки согласия.

  9. На последней странице Google покажет код авторизации.

get_token.js
  1. . Вставьте код в терминал и нажмите Enter.

  2. . После этого скрипт создаст файл token.json в папке, где вы запускали скрипт.

token.json

Скрипт генерации токена

const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');

const CREDENTIALS_PATH = 'credentials.json';
const TOKEN_PATH = 'token.json';


const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
const { client_secret, client_id, redirect_uris } = credentials.installed;

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);


const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];


const authUrl = oAuth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES,
});

console.log('Open this URL in browser:', authUrl);

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question('Enter the code from that page here: ', (code) => {
  rl.close();
  oAuth2Client.getToken(code, (err, token) => {
    if (err) return console.error('Error retrieving access token', err);
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
    console.log('Token stored to', TOKEN_PATH);
  });
});
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');

const CREDENTIALS_PATH = 'credentials.json';
const TOKEN_PATH = 'token.json';


const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
const { client_secret, client_id, redirect_uris } = credentials.installed;

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);


const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];


const authUrl = oAuth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES,
});

console.log('Open this URL in browser:', authUrl);

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question('Enter the code from that page here: ', (code) => {
  rl.close();
  oAuth2Client.getToken(code, (err, token) => {
    if (err) return console.error('Error retrieving access token', err);
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
    console.log('Token stored to', TOKEN_PATH);
  });
});

Заключение

В примерах в статье мы использовали язык программирования Node.js, полезные сниппеты на других языках вы найдете в документации

Документация Octo Browser

В этой статье мы рассмотрели:

  1. как подготовить среду разработки;

  2. как получить API-токен Octo Browser;

  3. как получить UUID расширения;

  4. как получить название профилей через API и добавить в текстовый документ;

  5. как массово добавлять расширения, закладки и стартовые страницы в существующие профили;

  6. как выгрузить данные профилей в Google Sheets.

Если у вас остались вопросы или вы не нашли нужного примера в документации — обращайтесь в службу поддержки в Telegram, чат на сайте или через виджет в клиенте.

Подготовка к работе

  1. Скачайте и установите VS Code.

  2. Скачайте и установите node.js.

  3. Создайте папку в удобном для вас месте и назовите ее, к примеру, octo_api.

  4. Откройте эту папку в VS Code.

  5. Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например, get_titles.js.

  6. Откройте терминал и выполните команду npm install axios, чтобы установить зависимость для NodeJS. 

  7. Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned и подтвердите. Затем повторите предыдущий пункт.

  8. Запустите клиент Octo Browser.

Где найти API-токен

Чтобы взаимодействовать с клиентом Octo Browser через API, вам потребуется API-токен. Он доступен пользователям с подпиской Base и выше. API-токен отображается в клиенте Octo Browser в настройках мастер-аккаунта на вкладке «Дополнительные» (другие члены команды API-токен не видят).

API-токен Octo Browser

Ограничения API (Rate Limits)

Стоит учитывать, что API Octo Browser имеет ограничения на количество запросов. В приведенных сниппетах используется функция check_limits, которая проверяет заголовки API-лимитов и автоматически делает паузу, если осталось мало запросов. Это помогает избежать ошибок 429 Too Many Requests.

При запросах к Public API тратится 1 RPM и 1 RPH за один запрос. При запросах к Local API RPM/RPH не снимаются, кроме запросов POST Start Profile (1 RPM и 1 RPH) и POST One-time profile (4 RPM и 4 RPH).

Точное число профилей, которые вы можете запустить/cоздать/удалить на определенной подписке, полностью зависит от ваших скриптов и их логики. Вы можете самостоятельно рассчитать необходимое количество запросов для вашей схемы работы, исходя из информации о том, какие запросы влияют на количество RPH и RPM.

Некоторые API-запросы могут требовать более сложной обработки, а значит, их выполнение может стоить больше, чем один запрос из лимитов. Это необходимо для балансировки нагрузки на сервер и оптимальной производительности для всех пользователей.

RPM (Requests Per Minute) — запросы в минуту.
RPH (Requests Per Hour) — запросы в час.
Значения лимитов зависят от вашей подписки.

С подготовкой разобрались, переходим к полезным сниппетам.

Получение имен профилей и сохранение их в .txt-файл

Этот сниппет будет полезен, когда нужно быстро собрать список всех созданных профилей в одном месте. Помогает упростить учет профилей, проверку их фактического наличия и дальнейшую работу с ними.

  1. В папке octo_api создайте файл get_titles.js в octo_api и скопируйте в него код сниппета. 

  2. В поле octo_token вместо OCTO_API_TOKEN вставьте свой API-токен из клиента Octo Browser.

OCTO_API_TOKEN
  1. Сохраните изменения в файле.

  2. Введите в терминал команду node get_titles.js и запустите скрипт. 

get_titles.js

Названия профилей будут сохранены построчно в файл profiles_titles.txt в папке, где находится скрипт. 

Сниппет для сохранения имен профилей в .txt-файл

const fs = require('fs').promises;
const axios = require('axios');
//This is the configuration. Specify your Octo API Token here
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url, 
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_titles(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function write_file(profiles) {
    const titles = profiles.map(item => item.title).filter(Boolean);
    try {
        await fs.writeFile('./profiles_titles.txt', titles.join('\n'), 'utf-8');
    } catch (error) {
        console.error(`ERROR: While trying writing the file some error occured:\n${error}`);
    }
}
(async () => {
    const total_pages = await get_total_pages();
    const profiles = await get_all_profiles_titles(total_pages);
    await write_file(profiles);
    console.log('Finished. Check profiles_titles.txt file...');
})()

Добавление расширений / стартовых страниц / закладок в профили

С помощью этой автоматизации вы сможете быстро привести конфигурацию профилей к единому виду и не добавлять одинаковые параметры вручную. 

Для добавления расширений вам потребуются их UUID:

  1. Откройте профиль с подключенным расширением.

  2. В адресной строке профиля введите chrome://extensions/ и нажмите Enter.

  3. На нужном расширении нажмите кнопку «Сведения» (Details).

chrome://extensions/
  1. Внизу страницы скопируйте UUID расширения вместе с версией.

UUID расширения

Cпособы для получения UUID с помощью API вы можете найти в документации.

Затем вам нужно отредактировать код:

  1. Создайте файл adding_info.js в папке octo_api.

  2. Скопируйте в него сниппет.

  3. В поле octo_token вставьте API-токен.

  4. В поле extensions (расширения) добавьте UUID необходимых расширений.

  5. В поле bookmarks (закладки) добавьте name — название закладки, URL — адрес сайта.

  6. В поле start_pages (стартовые страницы) добавьте URL сайтов, которые хотите открывать автоматически при запуске профиля. 

Если вам необходимо добавить не все, а только extensions/bookmarks/start_pages, — вы можете удалить ненужные вам параметры из сниппета:

data: {

        extensions: ["nkbihfbeogaeaoehlefnkodbefgpgknn@12.17.2"], //удаляем, если не нужно добавлять расширения

        bookmarks: [

            { name: "google", url: "https://google.com" },

            { name: "facebook", url: "https://facebook.com" }

        ],//удаляем, если не нужны закладки 

        start_pages: ["https://google.com", "https://facebook.com"] //удаляем, если не нужны стартовые страницы

    }
 adding_info.js
  1. Сохраните файл adding_info.js.

  2. Запустите файл с помощью команды node adding_info.js.

Нужные расширения, закладки и стартовые страницы будут добавлены во все ваши профили. Если изменения требуются для отдельных профилей — смотрите пример в документации.

adding_info.js

Перед запуском скрипта убедитесь, что профили, в которые необходимо добавить данные, не запущены. Запущенные профили обновлены не будут. 

Сниппет для добавления расширений, стартовых страниц и закладок в профили

const axios = require('axios');
// This is the configuration. Specify your Octo API Token here; you can modify this for your personal needs
const config = {
    octo_token: "OCTO_API_TOKEN", // Specify your API token here
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
    data: {
        "extensions": ["aapfglkgnhmiaabhiijkjpfmhllaodgp@4.2.3"],
        "bookmarks": [
            { "name": "google", "url": "https://google.com" },
            { "name": "facebook", "url": "https://facebook.com" }
        ],
        "start_pages": ["https://google.com", "https://facebook.com"]
    }
}
const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});
//functions
async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}
async function get_total_profiles() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}
async function get_all_profiles_uuids(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}
async function patch_all_profiles(profiles) {
    let updated = [];
    let not_updated = [];
    for (let profile of profiles) {
        try {
            const response = await OCTO_REMOTE_API.patch(`/profiles/${profile.uuid}`, config.data);
            await check_limits(response);
            updated.push(profile);
            console.log(`Successfully updated ${profile.uuid}`);
        } catch (error) {
            not_updated.push(profile);
            console.error(`ERROR: Can't patch profile ${profile.uuid}`);
        }
    }
    return [updated, not_updated];
}
(async () => {
    const total = await get_total_profiles();
    const profiles = await get_all_profiles_uuids(total);
    const [updated, not_updated] = await patch_all_profiles(profiles);
    console.log(`Finished process:\nUpdated: ${updated.length}\nNot updated: ${not_updated.length}`);
})();

Выгрузка данных профилей в Google Sheets

Данные ваших профилей можно выгрузить в таблицу. Так удобнее структурировать информацию при работе, сортировать и анализировать данные. Можно выгрузить в следующие параметры профиля: 

  • UUID,

  • теги,

  • имя,

  • описание.

  1. Создайте файл google_sheets.js в папке octo_api.

  2. Добавьте код сниппета в файл.

  3. Заполните поле octo_token — API-токен можно найти в клиенте Octo.

  4. Заполните поле table_id — это ID таблицы Google Sheets.

  5. Заполните поле table_sheet_name — это название листа внутри таблицы Google Sheets, куда вам нужна выгрузка.

google_sheets.js
  1. Сохраните изменения.

  2. Добавьте в папку octo_api файлы credentials.json и token.json.

  3. Запустите скрипт через терминал VS Code командой node google_sheets.js.

  4. После выполнения скрипта данные будут загружены в указанную таблицу.

    google_sheets.js

Сниппет для выгрузки данных профилей в Google Sheets

const axios = require('axios');
const { google } = require('googleapis');
const token = require('./token.json');
const credentials = require('./credentials.json');


const oAuth2Client = new google.auth.OAuth2(
    credentials.installed.client_id,
    credentials.installed.client_secret,
    credentials.installed.redirect_uris[0]
);


oAuth2Client.setCredentials(token);


//configs
//here is config. Paste your Octo API Token, table id and sheet name here
const config = {
    octo_token: "OCTO_API_TOKEN",
    table_id: "1pqnVysHymZSI5Lzx6odasdasAqE022fqUEf6q5LnOqAhylLSM",
    table_sheet_name: "Profiles",
    octo_api_base_url: "https://app.octobrowser.net/api/v2/automation/",
}


const OCTO_REMOTE_API = axios.create({
    baseURL: config.octo_api_base_url,
    timeout: 10000,
    headers: {
        'X-Octo-Api-Token': config.octo_token,
        'Content-Type': "application/json"
    }
});


//functions
async function check_limits(response) {
    function parse_int_safe(value) {
        const parsed = parseInt(value, 10);
        return isNaN(parsed) ? 0 : parsed;
    }
    const ratelimit_header = response.headers.ratelimit;
    if (!ratelimit_header) {
        console.warn('No ratelimit header found!');
        return;
    }
    const limit_entries = ratelimit_header.split(',').map(entry => entry.trim());
    for (const entry of limit_entries) {
        const name_match = entry.match(/^([^;]+)/);
        const r_match = entry.match(/;r=(\d+)/);
        const t_match = entry.match(/;t=(\d+)/);
        if (!r_match || !t_match) {
            console.warn(`Invalid ratelimit format: ${entry}`);
            continue;
        }
        const limit_name = name_match ? name_match[1] : 'unknown_limit';
        const remaining_quantity = parse_int_safe(r_match[1]);
        const window_seconds = parse_int_safe(t_match[1]);
        if (remaining_quantity < 5) {
            const wait_time = window_seconds + 1;
            console.log(`Waiting ${wait_time} seconds due to ${limit_name} limit`);
            await sleep(wait_time);
        }
    }
}


async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms * 1000));
}




async function get_total_pages() {
    const response = await OCTO_REMOTE_API.get('/profiles?page=0&page_len=10');
    const total_profiles = response.data.total_count;
    const total_pages = Math.ceil(response.data.total_count / 100);
    console.log(`Total Profiles: ${total_profiles}\nTotal Pages: ${total_pages}`);
    await check_limits(response);
    return total_pages;
}


async function get_all_profiles_data(total_pages) {
    let profiles = [];
    for (let i = 0; i < total_pages; i++) {
        let response = await OCTO_REMOTE_API.get(`/profiles?page=${i}&page_len=100&fields=title,description,tags`);
        await check_limits(response);
        profiles.push(...response.data.data);
    }
    return profiles;
}


async function fill_sheet_with_data(oAuth2Client, spreadsheet_id, list_name, data) {
    const sheets = google.sheets({ version: 'v4', auth: oAuth2Client });


    try {
        const sheetInfo = await sheets.spreadsheets.get({
            spreadsheetId: spreadsheet_id,
        });


        const sheetExists = sheetInfo.data.sheets.some(
            sheet => sheet.properties.title === list_name
        );


        if (!sheetExists) {
            await sheets.spreadsheets.batchUpdate({
                spreadsheetId: spreadsheet_id,
                resource: {
                    requests: [{
                        addSheet: {
                            properties: {
                                title: list_name
                            }
                        }
                    }]
                }
            });
            console.log(`Создан новый лист "${list_name}".`);
        }


        const headers = ['Uuid', 'Tags', 'Title', 'Description'];
        const values = data.map(item => [
            item.uuid,
            Array.isArray(item.tags) ? item.tags.join(', ') : item.tags,
            item.title,
            item.description || ''
        ]);


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A1:D1`,
            valueInputOption: 'RAW',
            resource: {
                values: [headers]
            }
        });


        await sheets.spreadsheets.values.update({
            spreadsheetId: spreadsheet_id,
            range: `${list_name}!A2:D${values.length + 1}`,
            valueInputOption: 'RAW',
            resource: {
                values: values
            }
        });


        console.log(`Данные успешно записаны в лист "${list_name}".`);


    } catch (err) {
        console.error('Ошибка при обновлении таблицы:', err);
        throw err;
    }
}


(async () => {
    const total_pages = await get_total_pages();
    const profiles_data = await get_all_profiles_data(total_pages);
    await fill_sheet_with_data(oAuth2Client, config.table_id, config.table_sheet_name, profiles_data);
})()

Как получить table_id

  1. Откройте или создайте таблицу в Google Sheets.

  2. Скопируйте ID таблицы — это часть в URL между /d/ и /edit.

Например, если ссылка: https://docs.google.com/spreadsheets/d/1GoPnhkStxFFxDzGZjjXSyjT_H_msHlSDx51tBROewOA/edit, то table_id: 1GoPnhkStxFFxDzGZjjXSyjT_H_msHlSDx51tBROewOA.

Как получить файл credentials.json

  1. Авторизуйтесь в персональный (не состоящий в организации) Gmail-аккаунт.

  2. Перейдите по ссылке https://console.cloud.google.com/.

  3. Выберите из списка любую европейскую страну и нажмите Agree and continue.

  4. Кликните Select a project, далее New project.

  5. Введите любое название проекта и нажмите Create.

  6. Перейдите по ссылке https://console.cloud.google.com/auth. Кликните Get Started.

  7. Укажите любое App Name, вашу почту и нажмите Next.

  8. Выберите External, еще раз укажите свою почту, кликните Next, активируйте чекбокс с согласием, затем нажмите Continue и Finish.

  9. Перейдите по ссылке https://console.cloud.google.com/auth/audience.

  10. В разделе Test Users нажмите кнопку Add Users.

Create OAuth Client
  1. . Добавьте вашу почту и нажмите Save.

  2. . На странице https://console.cloud.google.com/auth/overview нажмите Create OAuth Client.

Create OAuth Client
  1. . Выберите из списка Desktop app, введите любое название и кликните на Create.

  2. . Появится всплывающее окно, из которого нужно скачать JSON-файл.

Create OAuth Client
  1. . Перейдите по ссылке https://console.cloud.google.com/apis/library.

  2. . В строку поиска впишите Google Sheets API.

  3. . Выберите найденный API, затем нажмите Enable.

  4. . Откройте ранее скачанный JSON-файл.

  5. . Измените значение параметра redirect_uris с http://localhost на "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"]. 

Делайте замену аккуратно, чтобы не стереть скобки.

credentials.json
  1. . Сохраните файл.

  2. . Переименуйте файл в credentials.json.

  3. . Добавьте его в папку octo_api.

Как получить файл token.json

  1. Создайте в папке octo_api файл get_token.js и откройте в VS Code.

  2. Введите в терминал VS Code команду Npm install googleapis.

  3. Вставьте в файл get_token.js скрипт для генерации токена и сохраните.

  4. Запустите скрипт командой node get_token.js.

  5. Вы увидите ссылку в виде Open this URL in browser: https://accounts.google.com/o/oauth2/…

  6. Откройте ссылку.

  7. Войдите в свой аккаунт Google.

  8. Нажмите кнопки согласия.

  9. На последней странице Google покажет код авторизации.

get_token.js
  1. . Вставьте код в терминал и нажмите Enter.

  2. . После этого скрипт создаст файл token.json в папке, где вы запускали скрипт.

token.json

Скрипт генерации токена

const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');

const CREDENTIALS_PATH = 'credentials.json';
const TOKEN_PATH = 'token.json';


const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
const { client_secret, client_id, redirect_uris } = credentials.installed;

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);


const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];


const authUrl = oAuth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES,
});

console.log('Open this URL in browser:', authUrl);

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question('Enter the code from that page here: ', (code) => {
  rl.close();
  oAuth2Client.getToken(code, (err, token) => {
    if (err) return console.error('Error retrieving access token', err);
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
    console.log('Token stored to', TOKEN_PATH);
  });
});

Заключение

В примерах в статье мы использовали язык программирования Node.js, полезные сниппеты на других языках вы найдете в документации

Документация Octo Browser

В этой статье мы рассмотрели:

  1. как подготовить среду разработки;

  2. как получить API-токен Octo Browser;

  3. как получить UUID расширения;

  4. как получить название профилей через API и добавить в текстовый документ;

  5. как массово добавлять расширения, закладки и стартовые страницы в существующие профили;

  6. как выгрузить данные профилей в Google Sheets.

Если у вас остались вопросы или вы не нашли нужного примера в документации — обращайтесь в службу поддержки в Telegram, чат на сайте или через виджет в клиенте.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

©

2026

Octo Browser

©

2026

Octo Browser

©

2026

Octo Browser