Skip to content

Commit 3d6f1ad

Browse files
authored
fix tags e correções de funcionamento
1 parent 641ac2b commit 3d6f1ad

4 files changed

Lines changed: 205 additions & 23 deletions

File tree

rss-notifier/panel.luau

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,17 @@ render = function()
8484
body = ui.scroll({ gap = 6, flexGrow = 1 }, rows)
8585
end
8686

87+
local headerButtons = {}
88+
if #items > 0 then
89+
table.insert(headerButtons, ui.button({ glyph = "trash", variant = "ghost", controlSize = "sm", tooltip = "Limpar tudo", onClick = "onClearAllClicked" }))
90+
end
91+
table.insert(headerButtons, ui.button({ glyph = "refresh", variant = "ghost", controlSize = "sm", tooltip = "Atualizar agora", onClick = "onRefreshClicked" }))
92+
table.insert(headerButtons, ui.button({ glyph = "close", variant = "ghost", controlSize = "sm", onClick = "onCloseClicked" }))
93+
8794
panel.render(ui.column({ gap = 10, padding = 12, fill = true }, {
8895
ui.row({ align = "center", justify = "space_between" }, {
8996
ui.label({ text = "RSS/Atom Notifier", fontSize = 15, fontWeight = "bold" }),
90-
ui.row({ gap = 4 }, {
91-
ui.button({ glyph = "refresh", variant = "ghost", controlSize = "sm", tooltip = "Atualizar agora", onClick = "onRefreshClicked" }),
92-
ui.button({ glyph = "close", variant = "ghost", controlSize = "sm", onClick = "onCloseClicked" }),
93-
}),
97+
ui.row({ gap = 4 }, headerButtons),
9498
}),
9599
ui.separator({}),
96100
body,
@@ -113,6 +117,12 @@ function onRefreshClicked()
113117
noctalia.runAsync("noctalia msg plugin nilsonlinux/rss-notifier:fetcher all refresh")
114118
end
115119

120+
function onClearAllClicked()
121+
items = {}
122+
render()
123+
noctalia.runAsync("noctalia msg plugin nilsonlinux/rss-notifier:fetcher all clear-all")
124+
end
125+
116126
function onCloseClicked()
117127
panel.close()
118128
end

rss-notifier/plugin.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ author = "Nilsonlinux"
66
license = "MIT"
77
icon = "rss"
88
description = "Acompanha feeds RSS/Atom e notifica quando surgem novos itens."
9-
tags = ["utility"]
10-
dependencies = ["xdg-open"]
9+
tags = ["utility", "indicator"]
1110

1211
[[setting]]
1312
key = "feed_urls"

rss-notifier/service.luau

Lines changed: 190 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,149 @@
33
-- Atom <entry>), compara com o que ja foi visto, notifica o que for novo e
44
-- publica a contagem de nao-lidos em noctalia.state para o widget consumir.
55

6-
local seen = {} -- { [feedUrl] = { [itemId] = true, ... } }
6+
local seen = {} -- { [feedUrl] = { ids = { [itemId]=true, ... }, order = { id1, id2, ... } } }
77
local unread = 0
88
local recentItems = {} -- lista dos itens mais recentes, mais novo primeiro
99
local dataPath = nil
1010

11-
local MAX_RECENT_ITEMS = 50
11+
local MAX_RECENT_ITEMS = 50
12+
local MAX_SEEN_PER_FEED = 300 -- limite de ids "ja vistos" guardados por feed (evita crescer sem fim)
1213

1314
-- Limites para manter o parsing rapido o suficiente para o orcamento de CPU
1415
-- (muito apertado) do callback assincrono.
1516
local MAX_BODY_BYTES = 8000 -- so olhamos os primeiros ~8KB do corpo do feed
1617
local MAX_ITEM_BYTES = 400 -- corta cada bloco <item>/<entry> antes de extrair tags
1718
local MAX_ITEMS = 8 -- para de processar itens depois desse tanto
1819

20+
local function isUtf8Continuation(b)
21+
return b ~= nil and b >= 0x80 and b < 0xC0
22+
end
23+
24+
-- Corta a string em ate n bytes, mas nunca no meio de um caractere UTF-8
25+
-- multibyte (acentos, aspas curvas, emoji, etc.) - cortar assim gera bytes
26+
-- invalidos que o renderizador de texto (Pango) rejeita, deixando labels
27+
-- (e por tabela o painel inteiro) sem aparecer.
1928
local function truncate(s, n)
2029
if not s or #s <= n then
2130
return s
2231
end
23-
return s:sub(1, n)
32+
local cut = n
33+
-- recua enquanto o byte da posicao for byte de continuacao (0x80-0xBF)
34+
while cut > 0 and isUtf8Continuation(s:byte(cut)) do
35+
cut = cut - 1
36+
end
37+
-- se o byte que sobrou for o inicio de uma sequencia multibyte, so mantem
38+
-- se ela couber inteira dentro do limite n; senao descarta ele tambem
39+
local b = cut > 0 and s:byte(cut) or nil
40+
if b and b >= 0xC0 then
41+
local seqLen = (b >= 0xF0 and 4) or (b >= 0xE0 and 3) or 2
42+
if cut + seqLen - 1 > n then
43+
cut = cut - 1
44+
end
45+
end
46+
return s:sub(1, cut)
47+
end
48+
49+
-- Mesma logica de "nao corte no meio de UTF-8", mas trabalhando so com
50+
-- indices/bytes individuais (sem materializar substrings gigantes) - usado
51+
-- para limitar o tamanho de um bloco <item> sem copiar o conteudo inteiro
52+
-- antes de cortar.
53+
local function safeCutEnd(xml, endPos, minPos)
54+
local pos = endPos
55+
while pos > minPos do
56+
local b = xml:byte(pos)
57+
if not b then
58+
break
59+
end
60+
if b < 0x80 then
61+
break -- ascii, seguro
62+
elseif b >= 0xC0 then
63+
local seqLen = (b >= 0xF0 and 4) or (b >= 0xE0 and 3) or 2
64+
if pos + seqLen - 1 <= endPos then
65+
break -- sequencia cabe inteira ate endPos, seguro
66+
end
67+
pos = pos - 1 -- nao cabe inteira, descarta o lead byte tambem
68+
else
69+
pos = pos - 1 -- byte de continuacao, ainda no meio da sequencia
70+
end
71+
end
72+
return pos
73+
end
74+
75+
-- Testa se um item ja foi visto e, se nao, marca como visto - com limite de
76+
-- tamanho por feed (descarta o mais antigo quando passa do limite), para
77+
-- nunca deixar a estrutura "seen" crescer sem fim (e estourar o encode/save).
78+
local function markSeen(url, id)
79+
local bucket = seen[url]
80+
if not bucket or not bucket.ids then
81+
bucket = { ids = {}, order = {} }
82+
seen[url] = bucket
83+
end
84+
if bucket.ids[id] then
85+
return false -- ja visto
86+
end
87+
bucket.ids[id] = true
88+
table.insert(bucket.order, id)
89+
while #bucket.order > MAX_SEEN_PER_FEED do
90+
local oldest = table.remove(bucket.order, 1)
91+
bucket.ids[oldest] = nil
92+
end
93+
return true -- era novo
94+
end
95+
96+
local function isFirstRunForFeed(url)
97+
local bucket = seen[url]
98+
return not bucket or not bucket.order or #bucket.order == 0
99+
end
100+
101+
-- Remove/descarta qualquer sequencia de bytes que NAO seja UTF-8 valido.
102+
-- Necessario porque alguns feeds vem com o charset errado no servidor (bytes
103+
-- Latin-1/CP-1252 marcados como UTF-8, por exemplo) - nesse caso nem um corte
104+
-- perfeito resolve, o defeito ja vem no texto original.
105+
local function sanitizeUtf8(s)
106+
if not s then
107+
return s
108+
end
109+
local out = {}
110+
local i = 1
111+
local len = #s
112+
while i <= len do
113+
local b = s:byte(i)
114+
if b < 0x80 then
115+
out[#out + 1] = string.char(b)
116+
i = i + 1
117+
else
118+
local seqLen
119+
if b >= 0xF0 and b <= 0xF4 then
120+
seqLen = 4
121+
elseif b >= 0xE0 then
122+
seqLen = 3
123+
elseif b >= 0xC2 then
124+
seqLen = 2
125+
else
126+
seqLen = 0 -- lead byte invalido (0x80-0xC1)
127+
end
128+
129+
local valid = seqLen > 0 and (i + seqLen - 1) <= len
130+
if valid then
131+
for k = 1, seqLen - 1 do
132+
local cb = s:byte(i + k)
133+
if not cb or cb < 0x80 or cb >= 0xC0 then
134+
valid = false
135+
break
136+
end
137+
end
138+
end
139+
140+
if valid then
141+
out[#out + 1] = s:sub(i, i + seqLen - 1)
142+
i = i + seqLen
143+
else
144+
i = i + 1 -- byte invalido: descarta so ele e continua
145+
end
146+
end
147+
end
148+
return table.concat(out)
24149
end
25150

26151
-- ---------------------------------------------------------------------------
@@ -41,17 +166,57 @@ local function loadState()
41166
seen = decoded.seen or {}
42167
recentItems = decoded.recentItems or {}
43168
unread = decoded.unread or 0
169+
170+
-- migra formato antigo ({ [id]=true, ... } direto) para o novo
171+
-- ({ ids=..., order=... }), se necessario
172+
for url, bucket in pairs(seen) do
173+
if type(bucket) == "table" and not bucket.ids then
174+
local migrated = { ids = {}, order = {} }
175+
for id, v in pairs(bucket) do
176+
if v == true then
177+
migrated.ids[id] = true
178+
table.insert(migrated.order, id)
179+
end
180+
end
181+
seen[url] = migrated
182+
end
183+
end
184+
185+
-- sanitiza itens ja persistidos (podem ter sido salvos com UTF-8
186+
-- quebrado por versoes anteriores deste plugin, antes deste fix)
187+
for _, item in ipairs(recentItems) do
188+
if item.title then
189+
item.title = sanitizeUtf8(item.title)
190+
end
191+
if item.feedTitle then
192+
item.feedTitle = sanitizeUtf8(item.feedTitle)
193+
end
194+
end
44195
end
45196
end
46197
end
47198

199+
-- Nunca deixa uma falha de encode/escrita derrubar o entry (isso ja causou o
200+
-- service ser desativado apos varios erros seguidos no update()).
48201
local function saveState()
49-
if dataPath then
50-
noctalia.writeFile(dataPath, noctalia.json.encode({
51-
seen = seen,
52-
recentItems = recentItems,
53-
unread = unread,
54-
}))
202+
if not dataPath then
203+
return
204+
end
205+
local ok, encoded = pcall(noctalia.json.encode, {
206+
seen = seen,
207+
recentItems = recentItems,
208+
unread = unread,
209+
})
210+
if ok and type(encoded) == "string" then
211+
pcall(noctalia.writeFile, dataPath, encoded)
212+
end
213+
end
214+
215+
-- Idem para publicar em noctalia.state: nunca propaga erro pra cima.
216+
local function publishItems()
217+
local ok, encoded = pcall(noctalia.json.encode, recentItems)
218+
if ok and type(encoded) == "string" then
219+
noctalia.state.set("items", encoded)
55220
end
56221
end
57222

@@ -70,7 +235,7 @@ local function decodeEntities(s)
70235
s = s:gsub("&quot;", '"')
71236
s = s:gsub("&#39;", "'")
72237
s = s:gsub("&amp;", "&")
73-
return noctalia.string.trim(s)
238+
return sanitizeUtf8(noctalia.string.trim(s))
74239
end
75240

76241
local function stripCdata(s)
@@ -154,6 +319,11 @@ local function findNextBlock(xml, tag, fromPos, maxItemBytes)
154319
return nil, fromPos
155320
end
156321
local contentEnd = math.min(closeStart - 1, openEnd + maxItemBytes)
322+
if contentEnd < closeStart - 1 then
323+
-- so precisa corrigir a fronteira UTF-8 quando o corte foi mesmo pelo
324+
-- limite de bytes (nao pela tag de fechamento, que ja e uma fronteira segura)
325+
contentEnd = safeCutEnd(xml, contentEnd, openEnd)
326+
end
157327
local block = xml:sub(openEnd + 1, contentEnd)
158328
return block, closeStart + #closeTag
159329
end
@@ -185,15 +355,13 @@ end
185355
-- extracao de itens) para manter cada callback pequeno.
186356
local function finalizeFeed(f)
187357
local url = f.url
188-
seen[url] = seen[url] or {}
189-
local firstRun = next(seen[url]) == nil
358+
local firstRun = isFirstRunForFeed(url)
190359
local notifyEnabled = noctalia.getConfig("notify_new")
191360
local newCount = 0
192361

193362
for _, item in ipairs(f.items) do
194363
local id = item.id or item.link or item.title
195-
if id and not seen[url][id] then
196-
seen[url][id] = true
364+
if id and markSeen(url, id) then
197365
newCount = newCount + 1
198366

199367
if not firstRun then
@@ -218,7 +386,7 @@ local function finalizeFeed(f)
218386
if not firstRun and newCount > 0 then
219387
unread = unread + newCount
220388
noctalia.state.set("unread", unread)
221-
noctalia.state.set("items", noctalia.json.encode(recentItems))
389+
publishItems()
222390
end
223391

224392
saveState()
@@ -312,7 +480,7 @@ end
312480
loadState()
313481
applyInterval()
314482
noctalia.state.set("unread", unread)
315-
noctalia.state.set("items", noctalia.json.encode(recentItems))
483+
publishItems()
316484
fetchAll() -- primeira leitura: so marca como "vistos", sem notificar (firstRun)
317485

318486
function update()
@@ -347,6 +515,11 @@ function onIpc(_event, payload)
347515
break
348516
end
349517
end
350-
noctalia.state.set("items", noctalia.json.encode(recentItems))
518+
publishItems()
519+
saveState()
520+
elseif _event == "clear-all" then
521+
recentItems = {}
522+
publishItems()
523+
saveState()
351524
end
352525
end

rss-notifier/thumbnail.webp

3.46 KB
Loading

0 commit comments

Comments
 (0)