Skip to content

Commit f78caec

Browse files
committed
feat: offer Lista Hũ (Paraguay) in the recommended lists
Lista Hũ's CSV export is 4.6 MB and 24,000 rows, over the subscription limits. Its API's unpaginated /api/v1/lista/ takes an added_from filter, so the entry asks for reports since 2025-01-01 (about 1,800 rows and 575 KB). The parser learns the Spanish column names the site uses (numero, tipo, desc, comentarios), and the plan (595, trunk 0, 8 or 9 digits) drops the foreign numbers people reported there rather than storing them as +595. Catalog revision 2, signed.
1 parent 48ce1c7 commit f78caec

7 files changed

Lines changed: 72 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ All notable changes to CallShield will be documented in this file.
109109
sent, is still waiting to send, or didn't go out. Reported a number as
110110
spam by mistake? Tap Send not spam on that row and the correction goes out
111111
the same way. The list is kept on the phone only.
112+
- Recommended lists has Lista Hũ, Paraguay's community list, with the
113+
reports made since January 2025. Its numbers are stored as +595 numbers
114+
whatever country your phone is in, and ones from other countries that
115+
people reported there are left out rather than misread. Lists that name
116+
their columns in Spanish ("numero", "tipo", "comentarios") are read too.
112117

113118
## v1.11.0 (2026-09-30)
114119

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<p align="center">
1313
<a href="https://github.com/SysAdminDoc/CallShield/releases/latest"><img src="https://img.shields.io/github/v/release/SysAdminDoc/CallShield?style=flat-square&color=a6e3a1" alt="Release"></a>
1414
<img src="https://img.shields.io/badge/Spam%20Numbers-51%2C362-f38ba8?style=flat-square" alt="51,362 Numbers">
15-
<img src="https://img.shields.io/badge/JVM%20unit%20tests-1939-94e2d5?style=flat-square" alt="1939 JVM unit tests">
15+
<img src="https://img.shields.io/badge/JVM%20unit%20tests-1940-94e2d5?style=flat-square" alt="1940 JVM unit tests">
1616
<img src="https://img.shields.io/badge/Android-10%2B-89b4fa?style=flat-square" alt="Android 10+">
1717
<img src="https://img.shields.io/badge/License-MIT-cba6f7?style=flat-square" alt="MIT License">
1818
<img src="https://img.shields.io/badge/API%20Keys-None-fab387?style=flat-square" alt="No required API keys">
@@ -290,7 +290,7 @@ The same test runs 5,000 real smishing reports sampled from the IMC 2025 dataset
290290
- Selective backup/restore for rules, non-secret settings, and opt-in logs, plus CSV log export and auto-cleanup (7/14/30/90 days)
291291
- Database sync every 6 hours, a trending-feed check every 30 minutes, and a daily digest notification
292292
- External blocklist subscriptions (Settings > External blocklists) take HTTPS CSV, TXT or JSON number lists of up to 1 MB and 20,000 rows. Each list is fetched again once a day, or on the interval it declares in its header (`# Expires: 12 hours`, or `"expires": "12h"` in JSON), never more often than every six hours and at least weekly. A download that comes back empty or with under half the list's numbers isn't applied in the background. The list keeps its last good copy and says why on its row. Each row shows the list's name, the host it comes from, how many numbers it holds and when it last updated, and TalkBack reads it as one item. A row's switch turns its list off or on. Tapping the list's name does nothing. Removing a list takes its numbers out straight away, and Undo puts both back without downloading the list again
293-
- The same card opens with Recommended lists for places the bundled database barely covers: OpenCallShield (Colombia, MIT), SpamChile (Chile, GPL-2.0) and Turkish Spam Numbers (Turkey, GPL-3.0). Each row shows the country and a link to the license next to Add, and nothing is downloaded until you tap it. Adding one fetches it straight from its own repository. None of them is folded into CallShield's database. Each list declares its country's number plan, so a number written the local way (`3131918305`, `03395051735` or `00573390714583` in the Colombian list) is stored as +57 whatever country your phone is in. That holds for the same link typed in by hand, too. The catalog ships in the app and a signed copy is refreshed on every sync, so a list can be added between releases
293+
- The same card opens with Recommended lists for places the bundled database barely covers: OpenCallShield (Colombia, MIT), SpamChile (Chile, GPL-2.0), Turkish Spam Numbers (Turkey, GPL-3.0) and Lista Hũ (Paraguay, CC BY-NC-SA 4.0, reports since January 2025, since the whole list is past the size limit). Each row shows the country and a link to the license next to Add, and nothing is downloaded until you tap it. Adding one fetches it straight from its own repository. None of them is folded into CallShield's database. Each list declares its country's number plan, so a number written the local way (`3131918305`, `03395051735` or `00573390714583` in the Colombian list) is stored as +57 whatever country your phone is in. That holds for the same link typed in by hand, too. The catalog ships in the app and a signed copy is refreshed on every sync, so a list can be added between releases
294294
- If GitHub is blocked where you live, Settings > Feed mirror takes a second address for the protection data. CallShield asks GitHub first, then the mirror, then falls back to the copy bundled with the app. For ten minutes after GitHub couldn't be reached at all, the mirror goes first. One tap fills in jsDelivr (`https://cdn.jsdelivr.net/gh/SysAdminDoc/CallShield@master/`), which serves the same files and can run up to 12 hours behind. Mirrored files go through the same signature check, so a mirror can't alter the data, though it can hold back updates. [data/README.md](data/README.md#mirrors-and-recovery) covers running your own
295295
- Two Quick Settings tiles (protection on or off, and Expecting a call), app shortcuts and a home screen widget
296296
- Protection test validates all layers and permissions, including checker errors and deadline cutoffs
@@ -532,12 +532,12 @@ RELEASE_KEY_PASSWORD=...
532532
## Testing
533533

534534
```bash
535-
./gradlew testDebugUnitTest # 1939 tests
535+
./gradlew testDebugUnitTest # 1940 tests
536536
./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.sysadmindoc.callshield.platform.TargetSdkBehaviorSmokeTest
537537
./gradlew verifyPipelineTests # Cloudflare Worker (node) + data-pipeline and translation checks (python)
538538
```
539539

540-
The suite is **1939 total JVM unit tests**, run with Robolectric wherever a screen, a service or the database is involved.
540+
The suite is **1940 total JVM unit tests**, run with Robolectric wherever a screen, a service or the database is involved.
541541

542542
Two GitHub workflows run without building the app. **Validation** runs the Worker and
543543
pipeline suites on every push except report-only ones (`run-pipeline-tests.ps1 -CorrectnessOnly`),
@@ -574,7 +574,7 @@ language in [issue #7](https://github.com/SysAdminDoc/CallShield/issues/7).
574574
| Community API | Cloudflare Workers |
575575
| URL Safety | Local spam-domain data, with optional PhishTank and OpenPhish |
576576
| Verification | Local Gradle, lint, and release-artifact checks |
577-
| Tests | 1939 JVM unit tests (JUnit) |
577+
| Tests | 1940 JVM unit tests (JUnit) |
578578
| Strings | 1779 string resources and 48 plural groups (translation-ready) |
579579
| Accessibility | 100+ content descriptions, 48dp touch targets |
580580
| Min SDK | 29 (Android 10) |

‎app/src/main/java/com/sysadmindoc/callshield/data/ExternalBlocklistParser.kt‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ internal object ExternalBlocklistParser {
5555
const val MAX_SUBSCRIPTION_BYTES = 1L * 1024L * 1024L
5656
const val MAX_SUBSCRIPTION_ROWS = 20_000
5757

58-
private val numberFields = setOf("number", "phone", "phone_number", "phoneNumber", "msisdn")
59-
private val typeFields = setOf("type", "category", "label")
60-
private val descriptionFields = setOf("description", "comment", "name", "reason")
58+
// Spanish names too: Lista Hũ (Paraguay) exports "numero", "tipo" and "desc" or "Comentarios".
59+
private val numberFields = setOf("number", "phone", "phone_number", "phoneNumber", "msisdn", "numero")
60+
private val typeFields = setOf("type", "category", "label", "tipo")
61+
private val descriptionFields = setOf("description", "comment", "name", "reason", "desc", "descripcion", "comentario", "comentarios")
6162
private val arrayFields = setOf("numbers", "blocklist", "entries", "data")
6263

6364
private const val EXPIRES_HEADER_LINES = 50

‎app/src/test/java/com/sysadmindoc/callshield/data/ExternalBlocklistParserTest.kt‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sysadmindoc.callshield.data
22

3+
import com.sysadmindoc.callshield.data.model.ListNumberPlan
34
import org.junit.Assert.assertEquals
45
import org.junit.Assert.assertTrue
56
import org.junit.Test
@@ -75,6 +76,39 @@ class ExternalBlocklistParserTest {
7576
assertEquals("IRS spoof", parsed.numbers.first().description)
7677
}
7778

79+
@Test
80+
fun parseReadsListaHuSpanishFieldNames() {
81+
val paraguay = ListNumberPlan("PY", "595", "0", listOf(8, 9))
82+
val json =
83+
ExternalBlocklistParser.parse(
84+
rawUrl = "https://listahu.org/api/v1/lista/?added_from=2025-01-01",
85+
rawLabel = "Lista Hũ",
86+
body =
87+
"""
88+
[
89+
{"id": 22601, "numero": "595992856745", "tipo": "Estafa", "screenshot": "https://listahu.org/media/x.jpg",
90+
"desc": "Se hace pasar por un banco", "check": false, "added": "2026-10-09T11:17:41-03:00", "votsi": 0, "votno": 0},
91+
{"id": 22600, "numero": "0981234567", "tipo": "SPAM", "desc": ""}
92+
]
93+
""".trimIndent(),
94+
normalizeNumber = { paraguay.toInternational(it) ?: it },
95+
)
96+
val csv =
97+
ExternalBlocklistParser.parse(
98+
rawUrl = "https://listahu.org/descargar/csv/",
99+
rawLabel = "Lista Hũ",
100+
body = "\"#\",\"Numero\",\"Tipo\",\"Comentarios\",\"Captura\",\"Fecha_Denuncia\"\n" +
101+
"1,\"595985843100\",\"SPAM\",\"Llamadas grabadas\",\"https://listahu.org/media/d2.jpg\",\"2015-02-16 09:37\"\n",
102+
normalizeNumber = { paraguay.toInternational(it) ?: it },
103+
)
104+
105+
assertEquals(listOf("+595992856745", "+595981234567"), json.numbers.map { it.number })
106+
assertEquals("Estafa", json.numbers.first().type)
107+
assertEquals("Se hace pasar por un banco", json.numbers.first().description)
108+
assertEquals(listOf("+595985843100"), csv.numbers.map { it.number })
109+
assertEquals("Llamadas grabadas", csv.numbers.single().description)
110+
}
111+
78112
@Test
79113
fun parseRejectsUnsupportedUrlsAndOversizedBodies() {
80114
val badScheme =

‎data/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ a list, append its entry, raise `revision`, run `python scripts/feed_signing.py
312312
sign` and commit the file with its `.sig`. A list must fit the subscription
313313
limits (1 MB, 20,000 rows) and must never become a database source.
314314

315+
Lista Hũ's full export is 4.6 MB, so its entry asks the site's API for the
316+
reports since a fixed date (`added_from=2025-01-01`, about 575 KB in October
317+
2026, growing about 25 KB a month). Move the date forward a year, raise
318+
`revision` and sign again before the response nears 1 MB, around 2028.
319+
315320
`spam_numbers.txt` is signed the same way for anyone who takes the list
316321
without the app. It's written with the shards, holds every number whose
317322
sources allow redistribution (range prefixes stay out, since Saracroche's are

‎data/list_catalog.json‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"version": 1,
3-
"revision": 1,
3+
"revision": 2,
44
"lists": [
55
{
66
"id": "opencallshield-co",
@@ -49,6 +49,23 @@
4949
"license_url": "https://github.com/symbuzzer/Turkish-Spam-Numbers/blob/main/LICENSE",
5050
"format": "txt",
5151
"enabled_by_default": false
52+
},
53+
{
54+
"id": "listahu-py",
55+
"name": "Lista Hũ",
56+
"country": "PY",
57+
"calling_code": "595",
58+
"trunk_prefix": "0",
59+
"national_lengths": [
60+
8,
61+
9
62+
],
63+
"url": "https://listahu.org/api/v1/lista/?added_from=2025-01-01",
64+
"homepage": "https://listahu.org",
65+
"license": "CC BY-NC-SA 4.0",
66+
"license_url": "https://creativecommons.org/licenses/by-nc-sa/4.0/",
67+
"format": "json",
68+
"enabled_by_default": false
5269
}
5370
]
5471
}

‎data/list_catalog.json.sig‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
MEUCIQDedmAXpdKK44hh1b1dt+fyM/2S21Y3J68Njb31mjAWFAIgaeL0mJyjXw22fgs5lJRryEqjKpNhoige9Xj3XjofVxA=
1+
MEUCIQDApNLnC6PdbNlxfTDMloLUAN33Q8FpYxnAu5lIFQhTgwIgd+Pgy9mZDYp21D9KTFriKhKDK4QuI49ogAuj5NC+854=

0 commit comments

Comments
 (0)