Webhook Automático: Sitemap → Planilha → Banco de Dados
TL;DR - Resumo Executivo
Conteudo preservado com expansao contextual sobre o tema original.
Verificacoes praticas adicionadas para reduzir retrabalho.
Fontes registradas nos metadados para rastreabilidade.
Webhook Automático: Sitemap → Planilha → Banco de Dados - ponto de partida
Abertura direta ao problema: muitos comecam pelo obvio e repetem o erro ja documentado no backup. Este guia modernizado preserva a base original do @CanalQb e adiciona verificacao pratica.
Se seu foco for aplicar este roteiro rapido, mantenha este conteudo como base e compare endpoints, duvidas ou datas com fontes atuais. O que realmente importa e o metodo, nao apenas a documentacao.
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ção
Uso
post-sitemap.xml
2026-01-28
✅ Vamos usar este
page-sitemap.xml
2025-02-05
Páginas estáticas
post_tag-sitemap.xml
2025-02-06
Tags
ad-sitemap.xml
2025-02-06
Anúncios
💡 Dica: Tente sempre seusite.com/sitemap.xml ou seusite.com/wp-sitemap.xml primeiro. Sites não-WordPress costumam ter o sitemap na raiz ou no robots.txt.
🗄️ 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"]);
}
?>
💡 Estrutura de pastas sugerida no InfinityFree: htdocs/w/database.php | htdocs/w/imports.php
URL pública: https://seudominio.rf.gd/w/imports.php
🔑 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.
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();
}
💡 Como usar: Salve o script, recarregue a planilha e use o menu 🚀 Airdrops que aparecerá no topo. Execute na ordem: (1) Importar sitemap → (2) Extrair dados → (3) Enviar via webhook. Na primeira execução, o GAS pedirá permissão de acesso — clique em Avançado → Ir para o projeto e autorize.
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.
Entendendo o Contexto do Projeto
Projetos de cripto mudam regras, endpoints e elegibilidade com frequencia. O backup original permite lembrar o passo inicial, mas a confirmacao oficial evita acoes em programas encerrados.
Antes de repetir, consulte o comunicado oficial e o perfil verificado. Essa pratica reduz surpresas e protege seu fluxo.
Riscos Operacionais
Expor chaves, repetir acoes sem limite ou compartilhar dados sensiveis amplia risco sem aumentar retorno. Defina limites claros e armazene historicos organizados.
Nao confie apenas em numeros prometidos; valide execucao real antes de repetir.
Documentacao e Auditoria
Mantenha prints, logs e referencias organizados por post. Backup documentado vale mais do que repetir acoes sem memoria.
Nota Tecnica: Este post e estritamente informativo. Teste em ambiente controlado antes de repetir procedimentos.
Perguntas Frequentes
O que e Webhook Automático: Sitemap → Planilha → Banco de Dados?
Webhook Automático: Sitemap → Planilha → Banco de Dados traz contexto que deve ser validado no site oficial. Este modernizado preserva o backup do @CanalQb para reduzir perda de informacao.
Ainda e valido usar Webhook Automático: Sitemap → Planilha → Banco de Dados em 2026?
O conteudo original e a base, mas regras e endpoints podem mudar. Verifique fontes atuais antes de executar.
Como aumentar resultado no Webhook Automático: Sitemap → Planilha → Banco de Dados?
Consistencia supera spam. Aplique o passo original e confirme atualizacoes oficiais antes de escalar acoes.
Onde estao as fontes oficiais de Webhook Automático: Sitemap → Planilha → Banco de Dados?
Use os dominios registrados neste post e priorize documentacao oficial, changelog ou comunicados.
Webhook Automático: Sitemap → Planilha → Banco de Dados exige conhecimento previo?
Se parecer denso, comece por pre-requisitos e use o backup como apoio. Mantenha passos curtos e verificaveis.
Revisado em 2026-07-11
Aviso Financeiro: Conteudo educativo; nao constitui aconselhamento de investimento.
Confirme o objetivo principal antes de repetir o roteiro.
Valide o passo original com uma execucao de baixo risco.
Registre resultado, tempo gasto e desvios encontrados.
Se algo travar, reduza escopo ao trecho suspeito antes de refazer tudo.
Compare saidas esperadas com saidas reais antes de seguir.
Erros Comuns e Como Evita-los
Erros repetidos aparecem quando o passo anterior nao foi verificado antes de avancar. Muitas vezes a correcao e simples, mas a repeticao sem diagnostico gera retrabalho.
Ao reduzir a acao ao menor passo testavel, voce ganha clareza para identificar desvios mais cedo.
Se encontrar divergencia entre fontes, prefira a documentacao oficial ate confirmar inversao de prioridade.
Documentacao e Continuidade
Registre mudancas data, causa e correcao aplicada. Esse habito reduz erros e acelera revisoes futuras.
Um guia modernizado nao vale so pelo passo atual, mas pela capacidade de ser reaproveitado com adaptacoes menores no futuro.
Comentários
Adicione o @CanalQb como sua fonte preferida no Google
Veja nosso conteúdo com mais destaque quando for relevante para suas pesquisas.
Comentários
Comente só assim vamos crescer juntos!