Formulário de contato

Nome

E-mail *

Mensagem *

Este blog é um complemento do nosso canal no YouTube. Clique em @CanalQb para seguir e acompanhar nossos vídeos!

Imagem

Webhook Automático: Sitemap → Planilha → Banco de Dados

Webhook Automático: Sitemap → Planilha → Banco de Dados

Publicado por em


@CanalQb no YouTube


@CanalQb

Webhook Automático: Sitemap → Planilha → Banco de Dados


⚠️ Sempre crie uma frase de segurança única para jogos, testnets ou airdrops e evite usar sua carteira principal.


Tutorial Completo

📡 Webhook Automático: Sitemap → Google Sheets → Banco de Dados

Aprenda a construir do zero um pipeline que importa qualquer sitemap XML, extrai dados de cada página e envia tudo para um banco de dados via webhook PHP hospedado gratuitamente no InfinityFree.

Você já quis monitorar automaticamente todos os posts de um site, coletar metadados e armazená-los em um banco de dados sem pagar nada por isso? Neste tutorial do @CanalQb, você vai construir exatamente isso: um sistema que usa o Google Apps Script (GAS) como motor de automação, o Google Sheets como interface e fila de trabalho, e um arquivo PHP no InfinityFree como receptor de dados via webhook seguro.

O exemplo prático usa o site airdrops.one, mas você pode adaptar para qualquer site que possua um sitemap XML público. Ao final, você terá um sistema completo, com autenticação por chave secreta, prevenção de duplicatas e log de erros.

✅ O que você vai aprender

🗺️

Descobrir e ler sitemaps

Como identificar o sitemap correto de qualquer site (wp-sitemap.xml, sitemap_index.xml, post-sitemap.xml) e extrair todas as URLs.

🤖

Automatizar com GAS

Scripts no Google Apps Script que importam, processam e enviam dados sem precisar abrir o computador.

🐘

Criar banco de dados MySQL

Três tabelas completas com SQL pronto para executar no phpMyAdmin do InfinityFree.

🔐

Webhook seguro com PHP

Arquivo PHP que autentica requisições por chave secreta, evita duplicatas e registra logs.

♻️

Prevenção de duplicatas

Sistema que verifica se uma URL já existe antes de inserir, garantindo dados limpos.

🆓

100% gratuito

InfinityFree (PHP + MySQL), Google Sheets e Apps Script — sem custo algum.

⚙️ Como funciona o pipeline

1

GAS importa o sitemap XML

O script lê o arquivo post-sitemap.xml do site alvo, extrai todas as URLs e datas de modificação e registra na planilha Google Sheets, pulando URLs já existentes.

2

GAS raspa cada página

Para cada URL sem título ainda na planilha, o script acessa a página, extrai campos estruturados (título, token, descrição, eligibility, snapshot, tamanho do airdrop) via regex e preenche a linha correspondente.

3

GAS envia via webhook para o PHP

Com os dados prontos na planilha, outro script percorre as linhas ainda não enviadas e faz uma requisição POST autenticada para o arquivo imports.php hospedado no InfinityFree, que valida, desduplicata e persiste no MySQL.

🔍 Passo 1 — Descobrir o sitemap do site alvo

Todo site WordPress moderno expõe um índice de sitemaps em /wp-sitemap.xml. Ao acessar esse endereço você verá uma lista de sitemaps especializados. No caso do airdrops.one, o índice aponta para:

SitemapÚltima ModificaçãoUso
post-sitemap.xml2026-01-28✅ Vamos usar este
page-sitemap.xml2025-02-05Páginas estáticas
post_tag-sitemap.xml2025-02-06Tags
ad-sitemap.xml2025-02-06Anúncios

Neste tutorial usamos: https://airdrops.one/post-sitemap.xml

🗄️ Passo 2 — Criar as tabelas no MySQL (InfinityFree / phpMyAdmin)

Acesse o painel do InfinityFree, abra o phpMyAdmin, selecione seu banco de dados e execute os três blocos SQL abaixo na aba SQL. Você pode executá-los separadamente ou de uma vez.

Tabela 1 — import_imports (dados principais)

📋 SQL — import_imports
CREATE TABLE IF NOT EXISTS import_imports ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, url VARCHAR(2048) NOT NULL, last_modified VARCHAR(50) DEFAULT NULL, data_importacao TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, site_oficial VARCHAR(2048) DEFAULT NULL, titulo VARCHAR(512) NOT NULL, token VARCHAR(255) DEFAULT NULL, descricao_curta TEXT DEFAULT NULL, eligibility TEXT DEFAULT NULL, snapshot_dates VARCHAR(512) DEFAULT NULL, airdrop_size VARCHAR(255) DEFAULT NULL, descricao_completa LONGTEXT DEFAULT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', error_message TEXT DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, processed_at TIMESTAMP NULL DEFAULT NULL, UNIQUE KEY uq_url (url(768)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Tabela 2 — import_logs (log de operações)

📋 SQL — import_logs
CREATE TABLE IF NOT EXISTS import_logs ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, import_id INT UNSIGNED DEFAULT NULL, action VARCHAR(100) NOT NULL, request_data MEDIUMTEXT DEFAULT NULL, response_data MEDIUMTEXT DEFAULT NULL, ip_address VARCHAR(45) DEFAULT NULL, user_agent VARCHAR(512) DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_log_import FOREIGN KEY (import_id) REFERENCES import_imports(id) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Tabela 3 — import_settings (chave secreta)

📋 SQL — import_settings
CREATE TABLE IF NOT EXISTS import_settings ( id TINYINT UNSIGNED NOT NULL PRIMARY KEY, secret VARCHAR(128) NOT NULL, revealed_once TINYINT(1) NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, revealed_at TIMESTAMP NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
⚠️ O InfinityFree suporta MySQL 5.x/8.x. Certifique-se de que a coluna url usa UNIQUE KEY uq_url (url(768)) com prefixo de 768 bytes para evitar o erro de índice longo com utf8mb4.

🐘 Passo 3 — Criar o arquivo PHP no InfinityFree

No painel do InfinityFree, abra o File Manager (ou use FTP via FileZilla). Navegue até a pasta htdocs (ou uma subpasta, ex.: htdocs/w/) e crie dois arquivos: database.php e imports.php.

Arquivo 1 — database.php (conexão com o banco)

📋 PHP — database.php
<?php // Substitua pelos dados do seu banco no painel InfinityFree define('DB_HOST', 'sqlXXX.epizy.com'); // host do painel define('DB_NAME', 'epiz_XXXXXXX_nomedb'); // nome do banco define('DB_USER', 'epiz_XXXXXXX'); // usuário define('DB_PASS', 'SUA_SENHA'); // senha // Você pode definir a secret aqui (opcional — se ausente, será lida do banco) // define('IMPORT_SECRET', 'SUA_CHAVE_SECRETA'); class SimpleDB { private PDO $pdo; private ?PDOStatement $stmt = null; public function __construct() { $this->pdo = new PDO( 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] ); } public function query(string $sql): self { $this->pdo->query($sql); return $this; } public function prepare(string $sql): self { $this->stmt = $this->pdo->prepare($sql); return $this; } public function bind(int $pos, mixed $val): self { $this->stmt->bindValue($pos, $val); return $this; } public function execute(): bool { return $this->stmt->execute(); } public function fetch(): array|false { return $this->stmt->fetch(); } public function lastInsertId(): string { return $this->pdo->lastInsertId(); } } function db(): SimpleDB { static $instance = null; if ($instance === null) $instance = new SimpleDB(); return $instance; } ?>

Arquivo 2 — imports.php (webhook receptor)

Este é o coração do sistema. Ele faz três coisas: (1) na primeira abertura pelo navegador revela a chave secreta, (2) aceita requisições GET com ?token= para teste de autenticação, e (3) aceita requisições POST do GAS para inserir dados.

📋 PHP — imports.php (completo)
<?php header('Content-Type: application/json'); function w_log($m) { $f = __DIR__ . '/imports.debug.log'; $ip = $_SERVER['REMOTE_ADDR'] ?? 'na'; @file_put_contents($f, date('c') . " [$ip] " . $m . PHP_EOL, FILE_APPEND); } w_log('START method=' . ($_SERVER['REQUEST_METHOD'] ?? 'NA')); // ── GET: revelação da chave OU teste de autenticação ────────────────────────── if (($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') { w_log('GET_INIT'); // Teste via ?token= ou ?secret= if (isset($_GET['token']) || isset($_GET['secret'])) { $provided = $_GET['token'] ?? $_GET['secret'] ?? ''; $expected = null; if (defined('IMPORT_SECRET')) $expected = IMPORT_SECRET; if ($expected === null) { try { require_once __DIR__ . '/database.php'; $row = db()->prepare("SELECT secret FROM import_settings WHERE id = 1") ->execute()->fetch(); // Nota: encadeamento simplificado — veja nota abaixo } catch (Throwable $e) { w_log('GET_SECRET_FAIL ' . $e->getMessage()); } } header('Content-Type: application/json'); if ($expected === null) { http_response_code(503); echo json_encode(["error"=>"Secret não configurada"]); exit; } if ($provided !== $expected) { w_log('SECRET_FAIL'); http_response_code(403); echo json_encode(["error"=>"Acesso negado"]); exit; } w_log('GET_TEST_OK'); echo json_encode(["ok"=>true,"message"=>"Teste autorizado","ts"=>time()]); exit; } // Revelação única da chave (abertura sem token) header('Content-Type: text/plain; charset=utf-8'); try { require_once __DIR__ . '/database.php'; $dbi = db(); $row = $dbi->prepare("SELECT secret, revealed_once FROM import_settings WHERE id = 1")->execute()->fetch(); if (!$row) { $secret = bin2hex(random_bytes(32)); $dbi->prepare("INSERT INTO import_settings (id, secret, revealed_once) VALUES (1, ?, 0)") ->bind(1, $secret)->execute(); $row = ['secret' => $secret, 'revealed_once' => 0]; w_log('SECRET_CREATED'); } if ((int)$row['revealed_once'] === 0) { echo "IMPORT_SECRET=" . $row['secret'] . "\n"; echo "Guarde esta chave. Ao recarregar a página, ela não será exibida novamente.\n"; $dbi->prepare("UPDATE import_settings SET revealed_once=1, revealed_at=NOW() WHERE id=1")->execute(); w_log('SECRET_SHOWN'); } else { http_response_code(410); echo "Chave já gerada e ocultada. Use a chave salva.\n"; echo "Para redefinir, apague o registro id=1 em import_settings.\n"; } } catch (Throwable $e) { http_response_code(500); echo "Erro ao acessar import_settings.\n"; echo "Verifique se as tabelas foram criadas corretamente no phpMyAdmin.\n"; w_log('GET_TABLE_ERR ' . $e->getMessage()); } exit; } // ── OPTIONS preflight (CORS) ────────────────────────────────────────────────── if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(200); echo json_encode(["ok"=>true]); exit; } // ── Apenas POST além daqui ──────────────────────────────────────────────────── if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); echo json_encode(["error"=>"Método não permitido"]); exit; } require_once __DIR__ . '/database.php'; $dbi = db(); // Validar secret $sec = $_POST['secret'] ?? ''; $expected = 'MINHA_CHAVE_SECRETA'; if (defined('IMPORT_SECRET')) { $expected = IMPORT_SECRET; } else { try { $conf = $dbi->prepare("SELECT secret FROM import_settings WHERE id = 1")->execute()->fetch(); if ($conf && !empty($conf['secret'])) $expected = $conf['secret']; } catch (Throwable $e) { w_log('LOAD_SECRET_FAIL ' . $e->getMessage()); } } if ($sec !== $expected) { w_log('SECRET_FAIL'); http_response_code(403); echo json_encode(["error"=>"Acesso negado"]); exit; } // Coletar campos $cols = ['url','last_modified','site_oficial','titulo','token', 'descricao_curta','eligibility','snapshot_dates','airdrop_size','descricao_completa']; $vals = array_map(fn($c) => $_POST[$c] ?? null, $cols); // Título obrigatório if (trim((string)($vals[3] ?? '')) === '') { http_response_code(400); echo json_encode(["error"=>"titulo obrigatório"]); exit; } // Verificar tabela try { $dbi->query("SELECT 1 FROM import_imports LIMIT 0"); } catch (Throwable $e) { w_log('TABLE_MISSING ' . $e->getMessage()); http_response_code(500); echo json_encode(["error"=>"Tabela import_imports ausente"]); exit; } // Evitar duplicata por URL $urlVal = trim((string)($vals[0] ?? '')); if ($urlVal !== '') { $chk = $dbi->prepare("SELECT id FROM import_imports WHERE url = ? LIMIT 1")->bind(1, $urlVal)->execute()->fetch(); if ($chk && isset($chk['id'])) { w_log('SKIP_DUP url=' . substr($urlVal,0,120)); echo json_encode(["skipped"=>true,"id"=>$chk['id'],"reason"=>"URL já existe"]); exit; } } // Inserir try { $sql = "INSERT INTO import_imports (url,last_modified,data_importacao,site_oficial,titulo,token, descricao_curta,eligibility,snapshot_dates,airdrop_size,descricao_completa) VALUES (?,?,NOW(),?,?,?,?,?,?,?,?)"; $st = $dbi->prepare($sql); for ($i = 0; $i < count($vals); $i++) $st->bind($i + 1, $vals[$i]); $ok = $st->execute(); } catch (Throwable $e) { w_log('DB_ERROR ' . $e->getMessage()); http_response_code(500); echo json_encode(["error"=>"Falha no banco"]); exit; } if ($ok) { w_log('SUCCESS id=' . $dbi->lastInsertId()); echo json_encode(["success"=>true,"id"=>$dbi->lastInsertId()]); } else { http_response_code(500); echo json_encode(["error"=>"Falha ao inserir"]); } ?>

🔑 Passo 4 — Revelar a chave secreta

Depois de criar as tabelas e fazer o upload dos arquivos PHP, abra o navegador e acesse seu imports.php diretamente (sem parâmetros). Na primeira abertura, você verá:

IMPORT_SECRET=f405d2d23c74aa5f39e472e0bb7a198f53c391f740e5456cd1f435c7a25cbe06 Guarde esta chave. Ao recarregar a página, ela não será exibida novamente.
⚠️ Copie e salve essa chave imediatamente. Na próxima vez que abrir a página, ela não aparece mais. A chave fica armazenada no banco (tabela import_settings), mas nunca é re-exibida via browser por segurança.

Para testar se a autenticação funciona, acesse:
https://seudominio.rf.gd/w/imports.php?token=SUA_CHAVE
Se retornar {"ok":true,"message":"Teste autorizado"}, está tudo pronto.

Exemplo de teste com a chave de demonstração:
https://airdropsqb.free.nf/w/imports.php?token=f405d2d23c74aa5f39e472e0bb7a198f53c391f740e5456cd1f435c7a25cbe06

📊 Passo 5 — Configurar o Google Apps Script

Abra uma planilha Google Sheets, vá em Extensões → Apps Script e cole o código abaixo. Ele possui quatro funções principais que podem ser executadas pelo menu personalizado Airdrops ou pelos gatilhos automáticos do GAS.

Configurações no topo do script (edite antes de usar)

⚙️ GAS — Configurações globais
// ============================================================ // CONFIGURAÇÕES GLOBAIS — edite estas linhas // ============================================================ const SITEMAP_URL = "https://airdrops.one/post-sitemap.xml"; // URL do sitemap alvo const WEBHOOK_URL = "https://airdropsqb.free.nf/w/imports.php"; // URL do seu imports.php const SECRET = "f405d2d23c74aa5f39e472e0bb7a198f53c391f740e5456cd1f435c7a25cbe06"; // sua chave const SHEET_NAME = "airdrops"; // nome da aba na planilha const STATUS_COL = "Enviado_Airdrop"; // coluna de status de envio const FONT_SIZE = 8; const ROW_HEIGHT = 12; const SLEEP_MS = 500; // pausa entre requisições (ms) const HEADERS = [ "URL", // A "Last Modified", // B "Data Importação", // C "Site Oficial", // D "Título", // E "Token", // F "Descrição Curta", // G "Eligibility", // H "Snapshot Dates", // I "Airdrop Size", // J "Descrição Completa" // K ];

Script completo GAS

📋 GAS — Código completo (cole no Apps Script)
// ============================================================ // CONFIGURAÇÕES GLOBAIS // ============================================================ const SITEMAP_URL = "https://airdrops.one/post-sitemap.xml"; const WEBHOOK_URL = "https://airdropsqb.free.nf/w/imports.php"; const SECRET = "f405d2d23c74aa5f39e472e0bb7a198f53c391f740e5456cd1f435c7a25cbe06"; const SHEET_NAME = "airdrops"; const STATUS_COL = "Enviado_Airdrop"; const FONT_SIZE = 8; const ROW_HEIGHT = 12; const SLEEP_MS = 500; const HEADERS = [ "URL","Last Modified","Data Importação","Site Oficial", "Título","Token","Descrição Curta","Eligibility", "Snapshot Dates","Airdrop Size","Descrição Completa" ]; // ============================================================ // FUNÇÕES AUXILIARES // ============================================================ function _getSheet() { const ss = SpreadsheetApp.getActive(); return ss.getSheetByName(SHEET_NAME) || ss.getActiveSheet(); } function _ensureHeader(sheet) { if (sheet.getLastRow() === 0) { sheet.appendRow(HEADERS); sheet.getRange(1,1,1,HEADERS.length) .setFontSize(FONT_SIZE).setFontWeight("bold").setWrap(false); sheet.setRowHeights(1,1,ROW_HEIGHT); } } function _formatRows(sheet, startRow, numRows) { if (numRows <= 0) return; sheet.getRange(startRow,1,numRows,HEADERS.length) .setFontSize(FONT_SIZE).setWrap(true); sheet.setRowHeights(startRow,numRows,ROW_HEIGHT); } function _match(html, regex) { const m = html.match(regex); return m ? m[1].trim() : ""; } function _existingUrls(sheet) { const last = sheet.getLastRow(); if (last < 2) return new Set(); return new Set( sheet.getRange(2,1,last-1,1).getValues().flat().filter(String) ); } function ensureStatusColumn(sh) { const lastCol = sh.getLastColumn(); const headers = sh.getRange(1,1,1,lastCol).getValues()[0]; let idx = headers.findIndex(h => String(h).trim() === STATUS_COL); if (idx >= 0) return idx + 1; sh.getRange(1, lastCol+1).setValue(STATUS_COL); return lastCol + 1; } // ============================================================ // 1. IMPORTAR SITEMAP // ============================================================ function importarSitemap() { const sheet = _getSheet(); _ensureHeader(sheet); const existing = _existingUrls(sheet); const xml = UrlFetchApp.fetch(SITEMAP_URL).getContentText(); const root = XmlService.parse(xml).getRootElement(); const ns = root.getNamespace(); const urlNodes = root.getChildren("url", ns); const newRows = []; urlNodes.forEach(node => { const loc = node.getChildText("loc", ns); const lastmod = node.getChildText("lastmod", ns); if (loc && !existing.has(loc)) newRows.push([loc, lastmod, new Date(), "", "", "", "", "", "", "", ""]); }); if (newRows.length === 0) { SpreadsheetApp.getUi().alert("Nenhuma URL nova encontrada no sitemap."); return; } const startRow = sheet.getLastRow() + 1; sheet.getRange(startRow,1,newRows.length,HEADERS.length).setValues(newRows); _formatRows(sheet, startRow, newRows.length); SpreadsheetApp.getUi().alert(newRows.length + " URL(s) importada(s)."); } // ============================================================ // 2. EXTRAIR DADOS DOS POSTS (raspar cada página) // ============================================================ function extrairDadosPosts() { const sheet = _getSheet(); const lastRow = sheet.getLastRow(); if (lastRow < 2) return; const urlVals = sheet.getRange(2,1,lastRow-1,1).getValues(); const tituloVals= sheet.getRange(2,5,lastRow-1,1).getValues(); for (let i = 0; i < urlVals.length; i++) { const rowIndex = i + 2; const url = urlVals[i][0]; const titulo = tituloVals[i][0]; if (!url || titulo) continue; try { const html = UrlFetchApp.fetch(url, {muteHttpExceptions:true}).getContentText(); const siteOficial = _match(html, /post-page__buttons[\s\S]*?]*href="([^"]+)"/i); const tituloPost = _match(html, /class="post-page__title">([^<]+)]*>([^<]+)([^<]+)([^<]+)]*>([^<]+)]*>([^<]+)([\s\S]*?)<\/section>/i) .replace(/<[^>]+>/g,"").trim(); sheet.getRange(rowIndex,4,1,8).setValues([[ siteOficial, tituloPost, token, descricaoCurta, eligibility, snapshot, airdropSize, descCompleta ]]); _formatRows(sheet, rowIndex, 1); Utilities.sleep(SLEEP_MS); } catch(e) { Logger.log("Erro linha " + rowIndex + ": " + e.message); } } } // ============================================================ // 3. ENVIAR WEBHOOK — envia todas as linhas ainda não enviadas // ============================================================ function enviarAirdrops() { const sh = _getSheet(); if (!sh) return; const lastRow = sh.getLastRow(); if (lastRow < 2) return; const statusCol = ensureStatusColumn(sh); const rows = sh.getRange(2,1,lastRow-1,11).getValues(); const statuses = sh.getRange(2,statusCol,lastRow-1,1).getValues(); for (let i = 0; i < rows.length; i++) { // Pular linhas já enviadas com sucesso const currentStatus = String(statuses[i][0]).trim(); if (currentStatus === 'OK') continue; const r = rows[i]; const payload = { secret: SECRET, url: r[0] || '', last_modified: r[1] || '', data_importacao: r[2] || '', site_oficial: r[3] || '', titulo: r[4] || '', token: r[5] || '', descricao_curta: r[6] || '', eligibility: r[7] || '', snapshot_dates: r[8] || '', airdrop_size: r[9] || '', descricao_completa: r[10] || '' }; const result = _enviarPost(payload); sh.getRange(i+2, statusCol).setValue(result); Utilities.sleep(200); } SpreadsheetApp.getUi().alert("Envio concluído. Verifique a coluna " + STATUS_COL + "."); } // ============================================================ // 4. ENVIAR APENAS A LINHA SELECIONADA // ============================================================ function enviarLinhaSelecionada() { const sh = _getSheet(); if (!sh) return; const row = sh.getActiveRange().getRow(); if (row < 2) return; const statusCol = ensureStatusColumn(sh); const r = sh.getRange(row,1,1,11).getValues()[0]; const payload = { secret: SECRET, url: r[0] || '', last_modified: r[1] || '', data_importacao: r[2] || '', site_oficial: r[3] || '', titulo: r[4] || '', token: r[5] || '', descricao_curta: r[6] || '', eligibility: r[7] || '', snapshot_dates: r[8] || '', airdrop_size: r[9] || '', descricao_completa: r[10] || '' }; const result = _enviarPost(payload); sh.getRange(row, statusCol).setValue(result); SpreadsheetApp.getUi().alert("Resultado: " + result); } // ============================================================ // 5. EXECUTAR TUDO EM SEQUÊNCIA // ============================================================ function buscar() { importarSitemap(); extrairDadosPosts(); atualizarCabecalho(); } function atualizarCabecalho() { const sheet = _getSheet(); sheet.getRange(1,1,1,HEADERS.length) .setValues([HEADERS]) .setFontSize(FONT_SIZE).setFontWeight("bold").setWrap(false); sheet.setRowHeights(1,1,ROW_HEIGHT); } // ============================================================ // FUNÇÃO INTERNA: faz a requisição POST // ============================================================ function _enviarPost(payload) { try { const res = UrlFetchApp.fetch(WEBHOOK_URL, { method: 'post', payload: payload, muteHttpExceptions: true }); const code = res.getResponseCode(); const txt = res.getContentText(); if (code === 200) { try { const j = JSON.parse(txt); if (j && (j.success === true || j.skipped === true)) return 'OK'; if (j && j.error) return 'ERRO: ' + j.error; return 'ERRO: resposta inválida'; } catch(e) { return 'ERRO: JSON inválido'; } } else { try { const j = JSON.parse(txt); if (j && j.error) return 'ERRO: ' + j.error; } catch(e) {} return 'ERRO: HTTP ' + code; } } catch(e) { return 'ERRO: ' + e.message; } } // ============================================================ // MENU PERSONALIZADO // ============================================================ function onOpen() { SpreadsheetApp.getUi() .createMenu('🚀 Airdrops') .addItem('1 — Importar sitemap', 'importarSitemap') .addItem('2 — Extrair dados dos posts', 'extrairDadosPosts') .addSeparator() .addItem('3 — Enviar todos via webhook', 'enviarAirdrops') .addItem('4 — Enviar linha selecionada', 'enviarLinhaSelecionada') .addSeparator() .addItem('▶ Executar tudo (buscar)', 'buscar') .addToUi(); }

🔄 Resumo do fluxo completo

EtapaOndeO que acontece
1. Criar tabelas SQLInfinityFree → phpMyAdmin3 tabelas: imports, logs, settings
2. Upload dos PHPInfinityFree → File Managerdatabase.php + imports.php em htdocs/w/
3. Revelar chaveNavegador → URL do imports.phpChave exibida uma vez, salvar imediatamente
4. Testar autenticaçãoNavegador → URL + ?token=CHAVEDeve retornar {"ok":true}
5. Configurar GASGoogle Sheets → Apps ScriptColar código, ajustar SITEMAP_URL, WEBHOOK_URL, SECRET
6. Importar sitemapMenu Airdrops → passo 1URLs preenchem a planilha
7. Extrair dadosMenu Airdrops → passo 2Cada URL é acessada e os campos extraídos
8. Enviar webhookMenu Airdrops → passo 3Dados enviados ao PHP → MySQL. Coluna mostra OK

👥 Para quem é este tutorial

🧑‍💻

Desenvolvedores iniciantes

Que querem aprender automação com GAS e PHP de forma prática e gratuita.

🪙

Caçadores de airdrops

Que querem monitorar automaticamente novos projetos publicados em sites especializados.

📈

Analistas de dados

Que precisam coletar e estruturar dados de sites em banco relacional sem custo.

🏗️

Criadores de projetos pessoais

Que querem construir um pipeline de dados completo usando ferramentas gratuitas.

🚀 Pronto para automatizar?

Copie os scripts, crie as tabelas e comece agora mesmo. Todo o código é gratuito e open.

⚠️ Aviso: Este tutorial tem fins educativos. Ao raspar dados de sites externos, verifique sempre os Termos de Uso do site alvo e respeite o robots.txt. O @CanalQb não se responsabiliza pelo uso indevido das técnicas apresentadas. As chaves secretas exibidas neste post são de demonstração e foram invalidadas.

Marcadores:

© CanalQB – Tutoriais de YouTube, Python, Airdrops e Criptomoedas

Comentários