Back to Repository

Bra365

0 upvotes
By profissional.celiojunior@gmail.com
After page load (document_end)

Match Pattern

https://bra365.game/*

Script Code

(() => {
  const previousInput = document.querySelector("#lottery-panel textarea, #loteria-painel textarea");
  if (previousInput?.disabled) {
    console.warn("A purchase is in progress.");
    return;
  }

  window.__lotteryWidgetCleanup?.();
  window.__dxLoteriaPararColeta?.();
  document.getElementById("lottery-float")?.remove();
  document.getElementById("lottery-panel")?.remove();
  document.getElementById("loteria-flutuante")?.remove();
  document.getElementById("loteria-painel")?.remove();
  document.getElementById("dx-loteria-v2")?.remove();
  document.getElementById("lottery-widget-style")?.remove();

  // Remove only the app-update promotion, never unrelated dialogs.
  const removeUpdatePopup = () => {
    for (const dialog of document.querySelectorAll(".react-dialog-box")) {
      const text = dialog.textContent || "";
      const promotionTitle = /descobrir uma nova vers/i.test(text);
      const downloadPromotion = Boolean(
        dialog.querySelector('[style*="website/download/"]')
      ) && /atualizar/i.test(text);

      if (promotionTitle || downloadPromotion) {
        dialog.remove();
      }
    }
  };

  removeUpdatePopup();

  const PRICE = 3;
  const records = new Map();
  const submittedThisSession = new Set();
  let verifiedActiveCount = null;
  let confirmedThisSession = 0;
  let uncertainPurchase = false;
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  const money = value =>
    value.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
  const gameText = item =>
    `${item.date}: ${item.numbers.map(n => String(n).padStart(2, "0")).join(" ")}`;

  const waitFor = async (test, timeout = 15000) => {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      if (test()) return true;
      await delay(200);
    }
    return false;
  };

  const page = () => {
    const element = document.querySelector(
      '.page-stack__layer[aria-hidden="false"] .page-stack__route-content'
    );
    if (!element) throw Error("Página do sorteio indisponível");
    return element;
  };

  const grid = () => {
    const element = [...page().querySelectorAll(".grid.grid-cols-7")]
      .find(candidate =>
        candidate.children.length === 30 &&
        [...candidate.children].every(
          (child, index) => Number(child.textContent.trim()) === index + 1
        )
      );
    if (!element) throw Error("Grade não encontrada");
    return element;
  };

  const selected = () =>
    [...grid().children]
      .filter(element => !element.classList.contains("bg-[var(--act-bg-2709)]"))
      .map(element => Number(element.textContent.trim()))
      .sort((a, b) => a - b);

  const counter = () => {
    const element = [...page().querySelectorAll(".cursor-pointer")]
      .find(item => /^Meus Bilhetes\s*\d+$/.test(
        item.textContent.replace(/\s+/g, " ").trim()
      ));
    const match = element?.textContent.match(/Meus Bilhetes\s*(\d+)/);
    return match ? Number(match[1]) : null;
  };

  const clear = async () => {
    const button = [...grid().parentElement.querySelectorAll("*")]
      .find(element => element.children.length === 0 &&
        element.textContent.trim() === "Limpar");
    if (!button) throw Error("Botão Limpar não encontrado");
    button.click();
    if (!await waitFor(() => selected().length === 0, 3000)) {
      throw Error("Falha ao limpar seleção");
    }
  };

  const choose = async numbers => {
    await clear();
    for (const number of numbers) {
      const element = [...grid().children]
        .find(child => Number(child.textContent.trim()) === number);
      if (!element) throw Error(`Dezena ${number} não encontrada`);
      element.click();
      if (!await waitFor(() => selected().includes(number), 2000)) {
        throw Error(`Falha ao selecionar ${number}`);
      }
    }
    if (selected().join(",") !== numbers.join(",")) {
      throw Error("Seleção incorreta");
    }
  };

  const buyButton = () => {
    const matches = [...page().querySelectorAll(".btn_press")]
      .filter(button => /^ComprarcomR\$3(?:[,.]00)?$/.test(
        button.textContent.replace(/\s+/g, "")
      ));
    if (matches.length !== 1) throw Error("Botão Comprar não encontrado");
    return matches[0];
  };

  const parseGames = text => {
    const lines = text.split(/\r?\n/).map(x => x.trim()).filter(Boolean);
    if (!lines.length) throw Error("Nenhum jogo informado");

    const seen = new Set();
    const games = [];
    let duplicates = 0;

    lines.forEach((line, index) => {
      const match = line.match(/^(\d+)\s*:\s*(.+)$/);
      if (!match) throw Error(`Formato inválido na linha ${index + 1}`);
      const tokens = match[2].trim().split(/[\s,;]+/);
      if (tokens.length !== 5 || tokens.some(x => !/^\d+$/.test(x))) {
        throw Error(`Jogo ${match[1]}: são necessárias 5 dezenas`);
      }

      const numbers = tokens.map(Number).sort((a, b) => a - b);
      if (numbers.some(x => x < 1 || x > 30) || new Set(numbers).size !== 5) {
        throw Error(`Jogo ${match[1]}: dezenas inválidas`);
      }

      const key = numbers.join(",");
      if (seen.has(key)) {
        duplicates++;
        return;
      }

      seen.add(key);
      games.push({ label: Number(match[1]), numbers });
    });

    return { games, duplicates };
  };

  // Identify the blue share action inside a ticket card for accounting only.
  const hasShareAction = row => [...row.querySelectorAll("*")].some(element => {
    const label = [
      element.getAttribute("aria-label"),
      element.getAttribute("title")
    ].filter(Boolean).join(" ");

    if (/\b(compartilhar|compartilhe|share)\b/i.test(label)) return true;

    if (!element.querySelector("svg, img, i")) return false;
    if (element.textContent.trim().length > 2) return false;

    const color = getComputedStyle(element).backgroundColor;
    const match = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
    if (!match) return false;

    const [, red, green, blue] = match.map(Number);
    return blue >= 150 && blue > red * 1.5 && blue > green * 1.2 && green >= 65;
  });

  // Read each ticket card, including hits, prize and redemption state.
  const readRow = row => {
    const spans = [...row.querySelectorAll("span")];
    const texts = spans.map(span => span.textContent.trim());
    const date = texts.find(text => /^20\d{6}$/.test(text));
    const numbers = [...row.querySelectorAll("span.rounded-full")]
      .map(span => span.textContent.trim())
      .filter(text => /^\d{1,2}$/.test(text))
      .map(Number)
      .sort((a, b) => a - b);

    if (!date || numbers.length !== 5 ||
        new Set(numbers).size !== 5 ||
        numbers.some(number => number < 1 || number > 30)) return null;

    const hitsText = texts.find(text => /^[0-5]\s*\/\s*5\s*ACERTOS?$/i.test(text));
    const hits = hitsText ? Number(hitsText.match(/^([0-5])/)[1]) : null;
    const prizeText = texts.find(text => /^R\$\s*[\d.]+(?:,\d{2})?$/.test(text));
    const prize = prizeText
      ? Number(prizeText.replace(/[^\d,.]/g, "").replace(/\./g, "").replace(",", "."))
      : null;

    const label = texts.find(text => /^(Aguardando|Resgatado|Resgatada|Premiado|Premiada|Não premiado|Não premiada|Expirado|Expirada|Pendente|Concluído|Concluída|Perdido|Perdeu)$/i.test(text));
    const status = row.querySelector(".lottery-awaiting-pulse")
      ? "Aguardando"
      : label || "Não identificado";

    return { date: Number(date), numbers, hits, prize, status, paid: hasShareAction(row) };
  };

  const sameDetails = (a, b) =>
    a.status === b.status && a.hits === b.hits && a.prize === b.prize && a.paid === b.paid;

  // Keep the highest simultaneously observed multiplicity of each game.
  // There is no ticket ID in the card HTML.
  const scan = () => {
    const visible = new Map();
    for (const row of document.querySelectorAll(".rv-list > div")) {
      const ticket = readRow(row);
      if (!ticket) continue;
      const key = `${ticket.date}|${ticket.numbers.join(",")}`;
      if (!visible.has(key)) visible.set(key, []);
      visible.get(key).push(ticket);
    }

    for (const [key, observed] of visible) {
      const saved = records.get(key) || [];
      const used = new Set();

      for (const ticket of observed) {
        let index = saved.findIndex((item, i) =>
          !used.has(i) && sameDetails(item, ticket)
        );
        if (index < 0) index = saved.findIndex((_, i) => !used.has(i));

        if (index < 0) {
          index = saved.length;
          saved.push(ticket);
        } else {
          const old = saved[index];
          saved[index] = {
            ...ticket,
            hits: ticket.hits ?? old.hits,
            prize: ticket.prize ?? old.prize,
            paid: ticket.paid,
            status: ticket.status === "Não identificado" ? old.status : ticket.status
          };
        }
        used.add(index);
      }
      records.set(key, saved);
    }
    updateSummary();
  };

  const isWinner = item =>
    (item.hits !== null && item.hits >= 2) ||
    (item.prize !== null && item.prize > 0) ||
    /^(Resgatado|Resgatada|Premiado|Premiada)$/i.test(item.status);

  const allTickets = () => [...records.values()].flat().sort((a, b) =>
    b.date - a.date ||
    a.numbers.join(",").localeCompare(b.numbers.join(","), "en", { numeric: true })
  );

  const numbersKey = numbers => numbers.join(",");
  const activeNumberKeys = () => new Set(
    allTickets()
      .filter(item => item.status === "Aguardando")
      .map(item => numbersKey(item.numbers))
  );

  // The tab counter represents awaiting tickets, not the complete history.
  // Only awaiting tickets are relevant for checking current duplicate plays.
  const collectedActiveCount = () =>
    allTickets().filter(item => item.status === "Aguardando").length;

  const historyProgress = () => {
    let reportedCount = null;
    try {
      reportedCount = counter();
    } catch {
      // The history route may not expose the counter.
    }

    return {
      collectedCount: collectedActiveCount(),
      expectedCount: reportedCount === null
        ? (verifiedActiveCount === null
          ? null
          : verifiedActiveCount + confirmedThisSession)
        : reportedCount
    };
  };

  const requireVerifiedHistory = () => {
    if (uncertainPurchase) {
      throw Error("Compra anterior incerta. Confira os bilhetes antes de tentar novamente");
    }

    const reportedCount = counter();
    if (reportedCount === null) {
      throw Error("Contador de apostas ativas indisponível");
    }

    const collectedCount = collectedActiveCount();

    if (verifiedActiveCount === null) {
      if (collectedCount !== reportedCount) {
        throw Error(
          `Apostas ativas não conferidas (${collectedCount}/${reportedCount}). ` +
          "Abra Meus Bilhetes e percorra toda a lista antes de apostar"
        );
      }
      verifiedActiveCount = reportedCount;
    }

    // The counter must move exactly one unit for every confirmed purchase.
    const expectedCount = verifiedActiveCount + confirmedThisSession;
    if (reportedCount !== expectedCount) {
      throw Error(
        `Quantidade de apostas ativas mudou (${reportedCount}/${expectedCount}). ` +
        "Confira Meus Bilhetes antes de apostar"
      );
    }

    // The initial active list may be supplemented by new purchases.
    // It must never shrink or exceed the verified counter during the batch.
    if (collectedCount < verifiedActiveCount || collectedCount > expectedCount) {
      throw Error("Coleta de apostas ativas divergiu. Confira Meus Bilhetes");
    }

    return reportedCount;
  };

  const totals = () => {
    const items = allTickets();
    const winners = items.filter(isWinner);
    const prize = winners.reduce((sum, item) => sum + (item.prize ?? 0), 0);
    const redeemed = winners.filter(item => /^Resgatad[oa]$/i.test(item.status))
      .reduce((sum, item) => sum + (item.prize ?? 0), 0);
    const invested = items.reduce((sum, item) => sum + (item.paid ? PRICE : 0), 0);
    return {
      tickets: items.length,
      active: items.filter(item => item.status === "Aguardando").length,
      winners: winners.length,
      invested: Number(invested.toFixed(2)),
      prizes: Number(prize.toFixed(2)),
      redeemed: Number(redeemed.toFixed(2)),
      partial_net: Number((prize - invested).toFixed(2))
    };
  };

  const report = () => {
    scan();
    const items = allTickets();
    return JSON.stringify({
      summary: totals(),
      active: items.filter(item => item.status === "Aguardando").map(gameText),
      winners: items.filter(isWinner).map(item => ({
        game: gameText(item),
        hits: item.hits,
        prize: item.prize,
        status: item.status
      })),
      other: items.filter(item => item.status !== "Aguardando" && !isWinner(item))
        .map(item => ({ game: gameText(item), status: item.status }))
    }, null, 2);
  };

  const download = content => {
    const blob = new Blob([content], { type: "application/json;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    link.href = url;
    link.download = `loteria-resultados-${timestamp}.json`;
    link.style.display = "none";
    document.body.append(link);
    link.click();
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 5000);
  };

  // Minimal floating interface; rows are captured while the user scrolls.
  const style = document.createElement("style");
  style.id = "lottery-widget-style";
  style.textContent = `
    #lottery-float{position:fixed;top:16px;right:16px;width:34px;height:34px;
      padding:0;border-radius:50%;border:1px solid #ffffff60;background:#28282833;
      backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);color:white;
      font:19px/32px Arial,sans-serif;text-align:center;cursor:pointer;z-index:2147483646}
    #lottery-panel{position:fixed;top:60px;right:12px;
      width:min(340px,calc(100vw - 24px));max-height:calc(100dvh - 75px);
      overflow-y:auto;padding:14px;border-radius:12px;box-sizing:border-box;
      background:#202020;color:white;box-shadow:0 8px 30px #0008;
      font:14px Arial,sans-serif;z-index:2147483646;display:none}
    #lottery-panel textarea{width:100%;height:165px;padding:10px;
      border:1px solid #555;border-radius:8px;box-sizing:border-box;
      background:#303030;color:white;font:14px monospace;resize:vertical}
    #lottery-panel button{width:100%;margin-top:9px;padding:11px;border:0;
      border-radius:8px;cursor:pointer;background:#16a34a;color:white;font-weight:bold}
    #lottery-panel button:disabled{opacity:.5;cursor:default}
    #lottery-panel #lottery-download{background:#2563eb}
    #lottery-summary,#lottery-status{margin-top:9px;font-size:12px;color:#ccc;
      line-height:1.5;white-space:pre-line;overflow-wrap:anywhere}
  `;
  document.head.append(style);

  const floatingButton = document.createElement("button");
  floatingButton.id = "lottery-float";
  floatingButton.textContent = "+";
  floatingButton.title = "Apostas";

  const panel = document.createElement("div");
  panel.id = "lottery-panel";
  const title = document.createElement("div");
  title.textContent = "Apostas";
  title.style.cssText = "font-weight:bold;margin-bottom:10px";
  const input = document.createElement("textarea");
  input.placeholder = "1: 01 03 06 19 23\n2: 01 03 24 25 26";
  const placeButton = document.createElement("button");
  placeButton.textContent = "Apostar";
  const copyButton = document.createElement("button");
  copyButton.id = "lottery-download";
  copyButton.textContent = "Baixar JSON";
  const summary = document.createElement("div");
  summary.id = "lottery-summary";
  const status = document.createElement("div");
  status.id = "lottery-status";
  panel.append(title, input, placeButton, copyButton, summary, status);
  document.body.append(floatingButton, panel);

  let messageTimeout;
  let purchasing = false;

  function notify(text, duration = 2000) {
    clearTimeout(messageTimeout);
    status.textContent = text;
    if (duration > 0) {
      messageTimeout = setTimeout(() => { status.textContent = ""; }, duration);
    }
  }

  function updateSummary() {
    const t = totals();
    const { collectedCount, expectedCount } = historyProgress();
    const historyLabel = verifiedActiveCount !== null
      ? `Ativos conferidos: ${verifiedActiveCount} + ${confirmedThisSession} compra(s)`
      : expectedCount === null
        ? `Ativos capturados: ${collectedCount} (contador indisponível)`
        : collectedCount === expectedCount
          ? `Ativos capturados: ${collectedCount}/${expectedCount}`
          : collectedCount < expectedCount
            ? `Coleta de ativos: ${collectedCount}/${expectedCount}`
            : `Contagem de ativos divergente: ${collectedCount}/${expectedCount}`;

    summary.textContent =
      `${t.tickets} bilhetes · ${t.active} ativos · ${t.winners} premiados\n` +
      `Investido: ${money(t.invested)} · Prêmios: ${money(t.prizes)}\n` +
      `Resgatado: ${money(t.redeemed)} · Resultado parcial: ${money(t.partial_net)}\n` +
      historyLabel;
  }

  floatingButton.onclick = () => {
    panel.style.display = panel.style.display === "block" ? "none" : "block";
    scan();
  };

  copyButton.onclick = () => {
    scan();
    if (!records.size) return notify("Nenhum bilhete capturado", 3500);

    try {
      download(report());
      notify("Download iniciado", 2000);
    } catch (error) {
      console.error("JSON download failed", error);
      notify("Falha ao baixar JSON", 3500);
    }
  };

  placeButton.onclick = async () => {
    if (purchasing) return;

    let parsed;
    try {
      parsed = parseGames(input.value);
      buyButton();
      scan();
      requireVerifiedHistory();
    } catch (error) {
      notify(error.message, 6500);
      return;
    }

    // All awaiting tickets participate in the same current draw,
    // including those bought on previous dates.
    const activeKeys = activeNumberKeys();
    const pendingGames = [];
    let skipped = parsed.duplicates;

    for (const game of parsed.games) {
      const key = numbersKey(game.numbers);
      if (activeKeys.has(key) || submittedThisSession.has(key)) {
        skipped++;
      } else {
        pendingGames.push(game);
      }
    }

    if (!pendingGames.length) {
      input.value = "";
      notify(`Nenhuma aposta nova. ${skipped} duplicada(s) bloqueada(s)`, 5000);
      return;
    }

    purchasing = true;
    placeButton.disabled = true;
    input.disabled = true;

    // Collapse immediately after validation; purchases continue in sequence.
    panel.style.display = "none";
    floatingButton.textContent = "…";
    floatingButton.title = "Apostas em andamento";

    let completed = 0;
    let failed = false;

    for (const game of pendingGames) {
      const key = numbersKey(game.numbers);

      try {
        notify(`${completed + 1}/${pendingGames.length} · ${skipped} duplicada(s) ignorada(s)`, 0);
        const before = requireVerifiedHistory();

        // Re-check before clicking Buy. Even an uncertain submission is
        // remembered for this page session to prevent an automatic retry.
        if (activeNumberKeys().has(key) || submittedThisSession.has(key)) {
          skipped++;
          continue;
        }

        await choose(game.numbers);

        if (activeNumberKeys().has(key) || submittedThisSession.has(key)) {
          skipped++;
          continue;
        }

        const button = buyButton();
        submittedThisSession.add(key);
        uncertainPurchase = true;
        button.click();

        if (!await waitFor(() => counter() === before + 1, 15000)) {
          throw Error("Compra sem confirmação; confira Meus Bilhetes antes de tentar novamente");
        }

        uncertainPurchase = false;
        confirmedThisSession++;
        completed++;
        await delay(500);
      } catch (error) {
        failed = true;
        notify(`Jogo ${game.label}: ${error.message}`, 0);
        console.error("Purchase stopped", game, error);
        break;
      }
    }

    purchasing = false;
    placeButton.disabled = false;
    input.disabled = false;
    floatingButton.textContent = "+";
    floatingButton.title = "Apostas";

    if (failed) {
      // Keep unprocessed entries after a failure; the user must inspect
      // purchase history before retrying an uncertain transaction.
      panel.style.display = "block";
    } else {
      // Clear only after a completed operation; never auto-repeat the batch.
      input.value = "";
      notify(`${completed} comprada(s) · ${skipped} duplicada(s) bloqueada(s)`, 5000);
    }
  };

  const observer = new MutationObserver(mutations => {
    removeUpdatePopup();

    const changed = mutations.some(mutation => {
      const target = mutation.target.nodeType === 1
        ? mutation.target
        : mutation.target.parentElement;
      return Boolean(target?.closest?.(".rv-list") ||
        (mutation.addedNodes && [...mutation.addedNodes].some(node =>
          node.nodeType === 1 &&
          (node.matches?.(".rv-list") || node.querySelector?.(".rv-list"))
        )));
    });
    if (changed) scan();
  });

  observer.observe(document.body, {
    subtree: true,
    childList: true,
    characterData: true,
    attributes: true,
    attributeFilter: ["class"]
  });
  document.addEventListener("scroll", scan, true);
  const interval = setInterval(scan, 800);

  window.__lotteryWidgetCleanup = () => {
    observer.disconnect();
    document.removeEventListener("scroll", scan, true);
    clearInterval(interval);
    clearTimeout(messageTimeout);
    style.remove();
    floatingButton.remove();
    panel.remove();
  };

  scan();
})();
Install requires the InjectJS Chrome extension. Scripts run only on sites matching the pattern above. Review code before installing any community script.