SearchMore

Search in Bunker @ footboom.com forum

// ==UserScript==
// @name        SearchMore
// @namespace	novhna
// @description	Search in Bunker @ footboom.com forum
// @include     https://www.footboom.com/forum*
// @include     http://www.footboom.com/forum*
// @version     0.1.3
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

// ------------------------ HELPERS ---------------------------

// TODO to balance timeout and pages count
const updatingTimeout = 24 * 60 * 60 * 1000
const pagesToGrab = 10

const headers = {
    "content-type": "application/json",
    "x-apikey": "5b0953381af1a2243f0b9b1c",
    "cache-control": "no-cache",
}

const db = (collection, method, data = null) => new Promise((resolve, reject) => {
    fetch(`https://bunker-b54a.restdb.io/rest/${collection}`, { 
        method,
        headers, 
        mode: 'cors',
        body: data && JSON.stringify(data),
    })
        .then(res => res.json())
        .then(resolve)
        .catch(console.error)
})

// formatDate :: String -> String
const formatDate = date => (new Date(date)).toLocaleDateString()

// Should be created manually in DB at first
let meta = {
    lastUpdate: 0,
    lastPage: 0,
}

let topics = []

const grabPage = page => new Promise((resolve, reject) => {
    console.log('Grab page', page)
	fetch(`https://www.footboom.com/forum/bunker?page=${page}`)
        .then(res => res.text())
        .then(doc => { 
            const rows = doc.match(/<tr> <td class="m-10">[\s\S]*?<\/tr>/g)
            const info = rows.map(row => ({
                date: row.match(/datetime="(.+?)"/)[1], 
                topic: row.match(/<a[\s\S]*?>([\s\S]+?)<\/a>/)[1].trim(),
                author: row.match(/<td[\s\S]*?>(.+?)<\/td>/g)[2].replace(/<[\s\S]+?>/g, ''),
                link: row.match(/href="(.+?)"/)[1],
                page,
            }))
            resolve(info)
        })
        .catch(console.error)
})

const updateMetadata = lastPage => {
    meta.lastPage = lastPage
    meta.lastUpdate = Date.now()
    
    db(`metadata/${meta._id}`, 'PUT', meta).then(console.log)
    
    document.querySelector('#bunker-search--last-page').textContent = meta.lastPage
}

const getAllTopics = (callback = () => console.warn('Please provide a callback!')) => 
    db('topics', 'GET').then(callback)

const updateDB = (count, offset = 0) => {
    const pages = Array(count).fill().map((_, i) => i + 1 + +offset)
    
    Promise.all(pages.map(grabPage)).then(res => {
        const topics = res.reduce(($, arr) => [...$, ...arr], [])    
        console.log('Fetched topics:', topics)
        getAllTopics(savedTopics => {
            const savedLinks = savedTopics.map(({ link }) => link)
            const topicsToSave = topics.filter(({ link }) => !savedLinks.includes(link))
            const lastPage = Math.max(...savedTopics.map(({ page }) => +page))
            console.log(lastPage)
            
            db('topics', 'POST', topicsToSave).then(res => {
                updateMetadata(lastPage)
                console.log('Topics has been updated!', res)
            })
        })
    })
}

// ------------------- INTERACTIONS ------------------------

// Fetching metadata and update the index DB every 24 hours
const fetchMetadata = () => db('metadata', 'GET').then(docs => {
    meta = docs[0]
    document.querySelector('#bunker-search--last-page').textContent = meta.lastPage
    console.warn('Should update DB:', !(Date.now() - meta.lastUpdate < updatingTimeout))
    if (Date.now() - meta.lastUpdate < updatingTimeout) return false
    
    console.log('Updating topics in Database!')
    // Shallow indexing
    updateDB(pagesToGrab) 
})

// Indexing pages in range [startPage, endPage]
const indexInRange = (startPage, endPage) => {
    console.log('start:',endPage - startPage + 1,'offset:', startPage - 1)
    updateDB(endPage - startPage + 1, startPage - 1)
}

const searchFor = word => new Promise((resolve, reject) => {
    fetch(`https://bunker-b54a.restdb.io/rest/topics?q={"_tags":{"$regex":"${word}"}}`, {
        method: 'GET',
        mode: 'cors',
        headers,
    })
        .then(res => res.json())
        .then(resolve)
        .catch(console.error)

}) 

// ============Check if need to update DB ==================

fetchMetadata()
    
// ====================== VIEW =============================

// ======================= style ===========================

const style = document.createElement('style')
style.type = 'text/css'
style.innerHTML = `
    #bunker-search { 
        position:fixed; 
        left:0; 
        bottom:0; 
        right:0; 
        top: 0; 
        background-color: rgba(100,100,100,0.5); 
        color: black;
        z-index: -1;
        text-align: center;
        opacity: 0;
        
        transition: opacity 1s, z-index 0s 1s;
    }
    #bunker-search.expanded {
        opacity: 1;
        z-index: 9999;
        
        transition: opacity 1s, z-index 1s;
    }
    #bunker-search hr {
        border-top: 1px solid #fff;
    }

    #bunker-search--panel {
        display: inline-block;
        height: 100vh;
        width: 100vw;
        max-width: 700px;
        padding: 30px;
        background-color: #eee;
        transform: scale(0);

        transition: 1s;
    }
    .expanded #bunker-search--panel {
        transform: scale(1);
    }

    #bunker-search--results {
        overflow-y: auto; 
        overflow-x: hidden;
        text-align: left;
    }

    #bunker-search-control {
        position: fixed;
        bottom: 0;
        left: 0;
        background-color: green; 
        color: white;
        z-index: 99999;
        cursor: pointer;
    }
    #bunker-search-control .glass-image {
        height: 40px; 
        width: 40px; 
        background: url(https://www.footboom.net/img/new-images/icons/x2/searchx2.png) no-repeat center center;
        background-size: 17px 17px;
    }

    pre#bunker-search--last-page {
        display: inline;
    }

    .bunker-search--info {
        text-align: center;
    }

`
document.querySelector('head').appendChild(style);

// ================== results view =========================

const view = document.createElement('div')
view.id = 'bunker-search'

const submitForm = e => {
    e.preventDefault()

    view.querySelector('#bunker-search--results').innerHTML = `<li class="bunker-search--info">Шукаємо ${e.target.query.value}...</li>` 
    searchFor(e.target.query.value)
        .then(res => {
            const links = res.map(({ link, topic, author, date }) => `
                <li>[${formatDate(date)}] <a href="${link}">${topic}</a> <b>${author}</b></li>
            `).join('')
            view.querySelector('#bunker-search--results').innerHTML = links || '<li class="bunker-search--info">Нічого не знайдено</li>'
        })
}

const toggleSearchPanel = e => {
    view.classList[view.classList.contains('expanded') ? 'remove' : 'add']('expanded')
}

view.innerHTML = `
    <div id="bunker-search--panel">
        <form id="bunker-search--search">
            <input name="query" />
            <button>Шукати в Бункері</button>
        </form>
        <ul id="bunker-search--results"></ul>
        <hr />
        <div>
            Всього проіндексовано <pre id="bunker-search--last-page">...</pre> стор.<br />
            <form id="bunker-search--indexing">
                Індексувати з <input name="start" type="number" min="1" value="1" /> по <input name="end" type="number" min="1" value="1" /> стор. <button>Вперед</button>
            </form>
        </div>
    </div>
`
document.body.appendChild(view)
view.querySelector('form#bunker-search--search').onsubmit = submitForm
// Deep indexing
view.querySelector('form#bunker-search--indexing').onsubmit = e => {
    e.preventDefault()
    indexInRange(e.target.start.value, e.target.end.value)
}

// ==================== control ==========================

const control = document.createElement('div')
control.id = 'bunker-search-control'

control.onclick = toggleSearchPanel;
control.title = 'Search Bunker'
control.innerHTML = `<div class="glass-image"></div>`
document.body.appendChild(control)