code.pablomurad.com

skull-browser

Keyboard-driven browser for the small web, with native Gopher and Gemini

lib/adblock_bootstrap.lua raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
--- Keep the adblock filter lists on disk.
--
-- The adblock module reads `*.txt` files from its data directory and does
-- nothing else: it has no way to fetch a list, so a fresh install blocks
-- nothing at all. This module downloads the default lists on first run and
-- refreshes them once they go stale.
--
-- Downloads go through `curl` rather than the browser's own download stack,
-- which would show every refresh in the download list as if the user had asked
-- for it.
--
-- @module adblock_bootstrap
-- @copyright 2026 Pablo Murad <pablomurad@pm.me>

local lfs = require("lfs")
local adblock = require("adblock")
local modes = require("modes")

local _M = {}

--- Lists fetched by default.
-- Each entry needs a `file` ending in `.txt`, which is the only extension the
-- adblock module looks at.
-- @type table
-- @readwrite
_M.lists = {
    { file = "easylist.txt",
      uri = "https://easylist.to/easylist/easylist.txt" },
    { file = "easyprivacy.txt",
      uri = "https://easylist.to/easylist/easyprivacy.txt" },
    { file = "easylistportuguese.txt",
      uri = "https://easylist-downloads.adblockplus.org/easylistportuguese.txt" },
}

--- Days before a list on disk is considered stale.
-- @type number
-- @readwrite
_M.max_age_days = 7

local dir = luakit.data_dir .. "/adblock/"

local function age_days(path)
    local mtime = lfs.attributes(path, "modification")
    if not mtime then return nil end
    return (os.time() - mtime) / 86400
end

-- A truncated download or a captive-portal login page would otherwise be
-- parsed as filter rules without a word of complaint.
local function looks_like_filterlist(path)
    local f = io.open(path, "rb")
    if not f then return false end
    local head = f:read(256) or ""
    f:close()
    return head:match("^%[Adblock") ~= nil or head:match("^!") ~= nil
end

local pending = 0

local function finish()
    pending = pending - 1
    if pending > 0 then return end

    adblock.load(true)
    msg.info("adblock: listas recarregadas")
end

local function fetch(entry)
    local dest, part = dir .. entry.file, dir .. entry.file .. ".part"
    local cmd = string.format("curl -fsSL --max-time 120 -o %q %q", part, entry.uri)

    local ok, err = pcall(luakit.spawn, cmd, function (_, status)
        if status == 0 and looks_like_filterlist(part) then
            os.remove(dest)
            os.rename(part, dest)
            msg.info("adblock: %s atualizada", entry.file)
        else
            os.remove(part)
            msg.warn("adblock: falha ao baixar %s (status %s)", entry.file, tostring(status))
        end
        finish()
    end)

    if not ok then
        msg.warn("adblock: could not run curl: %s", tostring(err))
        finish()
    end
end

--- Download any list that is missing or stale.
-- @tparam[opt] boolean force Refresh every list regardless of age.
_M.update = function (force)
    if not lfs.attributes(dir, "mode") then lfs.mkdir(dir) end

    local todo = {}
    for _, entry in ipairs(_M.lists) do
        local age = age_days(dir .. entry.file)
        if force or not age or age > _M.max_age_days then
            todo[#todo + 1] = entry
        end
    end

    if #todo == 0 then
        msg.verbose("adblock: listas em dia")
        return 0
    end

    pending = #todo
    for _, entry in ipairs(todo) do fetch(entry) end
    return #todo
end

modes.add_cmds({
    { ":adblock-update", "Baixar ou atualizar as listas de filtro.",
        function (w)
            local n = _M.update(true)
            w:notify(string.format("adblock: baixando %d lista(s)...", n))
        end },
})

--- Schedule an update off the startup path.
-- `adblock.load` parses synchronously on the UI thread and EasyList alone is
-- over two megabytes, so this defers to idle. Call it from the config; the
-- module deliberately does nothing on require, which keeps the test suite off
-- the network.
_M.update_when_idle = function ()
    luakit.idle_add(function ()
        _M.update(false)
        return false
    end)
end

return _M

-- vim: et:sw=4:ts=8:sts=4:tw=80