mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-04-30 18:42:51 +02:00
Add new node ingest blocking
This commit is contained in:
@@ -367,6 +367,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
||||
await set_version(conn, 47)
|
||||
applied += 1
|
||||
|
||||
# Migration 48: Add discovery_blocked_types column to app_settings
|
||||
if version < 48:
|
||||
logger.info("Applying migration 48: add discovery_blocked_types to app_settings")
|
||||
await _migrate_048_discovery_blocked_types(conn)
|
||||
await set_version(conn, 48)
|
||||
applied += 1
|
||||
|
||||
if applied > 0:
|
||||
logger.info(
|
||||
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
|
||||
@@ -2909,3 +2916,25 @@ async def _migrate_047_add_statistics_indexes(conn: aiosqlite.Connection) -> Non
|
||||
"""
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def _migrate_048_discovery_blocked_types(conn: aiosqlite.Connection) -> None:
|
||||
"""Add discovery_blocked_types column to app_settings.
|
||||
|
||||
Stores a JSON array of integer contact type codes (1=Client, 2=Repeater,
|
||||
3=Room, 4=Sensor) whose advertisements should not create new contacts.
|
||||
Empty list means all types are accepted.
|
||||
"""
|
||||
try:
|
||||
await conn.execute(
|
||||
"ALTER TABLE app_settings ADD COLUMN discovery_blocked_types TEXT DEFAULT '[]'"
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if "duplicate column" in error_msg:
|
||||
logger.debug("discovery_blocked_types column already exists, skipping")
|
||||
elif "no such table" in error_msg:
|
||||
logger.debug("app_settings table not ready, skipping discovery_blocked_types migration")
|
||||
else:
|
||||
raise
|
||||
await conn.commit()
|
||||
|
||||
@@ -840,6 +840,13 @@ class AppSettings(BaseModel):
|
||||
default_factory=list,
|
||||
description="Display names whose messages are hidden from the UI",
|
||||
)
|
||||
discovery_blocked_types: list[int] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Contact type codes (1=Client, 2=Repeater, 3=Room, 4=Sensor) whose "
|
||||
"advertisements should not create new contacts; existing contacts are still updated"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FanoutConfig(BaseModel):
|
||||
|
||||
@@ -462,6 +462,20 @@ async def _process_advertisement(
|
||||
advert.device_role if advert.device_role > 0 else (existing.type if existing else 0)
|
||||
)
|
||||
|
||||
# Check discovery_blocked_types: skip new contacts whose type is blocked.
|
||||
# Existing contacts are always updated (location, name, last_seen, etc.).
|
||||
if existing is None and contact_type > 0:
|
||||
from app.repository import AppSettingsRepository
|
||||
|
||||
settings = await AppSettingsRepository.get()
|
||||
if contact_type in settings.discovery_blocked_types:
|
||||
logger.debug(
|
||||
"Skipping new contact %s: type %d is in discovery_blocked_types",
|
||||
advert.public_key[:12],
|
||||
contact_type,
|
||||
)
|
||||
return
|
||||
|
||||
# Keep recent unique advert paths for all contacts.
|
||||
await ContactAdvertPathRepository.record_observation(
|
||||
public_key=advert.public_key.lower(),
|
||||
|
||||
@@ -29,7 +29,7 @@ class AppSettingsRepository:
|
||||
SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert,
|
||||
sidebar_sort_order, last_message_times, preferences_migrated,
|
||||
advert_interval, last_advert_time, flood_scope,
|
||||
blocked_keys, blocked_names
|
||||
blocked_keys, blocked_names, discovery_blocked_types
|
||||
FROM app_settings WHERE id = 1
|
||||
"""
|
||||
)
|
||||
@@ -81,6 +81,14 @@ class AppSettingsRepository:
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
blocked_names = []
|
||||
|
||||
# Parse discovery_blocked_types JSON
|
||||
discovery_blocked_types: list[int] = []
|
||||
if row["discovery_blocked_types"]:
|
||||
try:
|
||||
discovery_blocked_types = json.loads(row["discovery_blocked_types"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
discovery_blocked_types = []
|
||||
|
||||
# Validate sidebar_sort_order (fallback to "recent" if invalid)
|
||||
sort_order = row["sidebar_sort_order"]
|
||||
if sort_order not in ("recent", "alpha"):
|
||||
@@ -98,6 +106,7 @@ class AppSettingsRepository:
|
||||
flood_scope=row["flood_scope"] or "",
|
||||
blocked_keys=blocked_keys,
|
||||
blocked_names=blocked_names,
|
||||
discovery_blocked_types=discovery_blocked_types,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -113,6 +122,7 @@ class AppSettingsRepository:
|
||||
flood_scope: str | None = None,
|
||||
blocked_keys: list[str] | None = None,
|
||||
blocked_names: list[str] | None = None,
|
||||
discovery_blocked_types: list[int] | None = None,
|
||||
) -> AppSettings:
|
||||
"""Update app settings. Only provided fields are updated."""
|
||||
updates = []
|
||||
@@ -163,6 +173,10 @@ class AppSettingsRepository:
|
||||
updates.append("blocked_names = ?")
|
||||
params.append(json.dumps(blocked_names))
|
||||
|
||||
if discovery_blocked_types is not None:
|
||||
updates.append("discovery_blocked_types = ?")
|
||||
params.append(json.dumps(discovery_blocked_types))
|
||||
|
||||
if updates:
|
||||
query = f"UPDATE app_settings SET {', '.join(updates)} WHERE id = 1"
|
||||
await db.conn.execute(query, params)
|
||||
|
||||
@@ -48,6 +48,13 @@ class AppSettingsUpdate(BaseModel):
|
||||
default=None,
|
||||
description="Display names whose messages are hidden from the UI",
|
||||
)
|
||||
discovery_blocked_types: list[int] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Contact type codes (1=Client, 2=Repeater, 3=Room, 4=Sensor) whose "
|
||||
"advertisements should not create new contacts"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BlockKeyRequest(BaseModel):
|
||||
@@ -122,6 +129,12 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
|
||||
if update.blocked_names is not None:
|
||||
kwargs["blocked_names"] = update.blocked_names
|
||||
|
||||
# Discovery blocked types
|
||||
if update.discovery_blocked_types is not None:
|
||||
# Only allow valid contact type codes (1-4)
|
||||
valid = [t for t in update.discovery_blocked_types if t in (1, 2, 3, 4)]
|
||||
kwargs["discovery_blocked_types"] = sorted(set(valid))
|
||||
|
||||
# Flood scope
|
||||
flood_scope_changed = False
|
||||
if update.flood_scope is not None:
|
||||
|
||||
@@ -409,44 +409,32 @@ export function Sidebar({
|
||||
[sortedChannels, query]
|
||||
);
|
||||
|
||||
const filteredNonRepeaterContacts = useMemo(
|
||||
() => {
|
||||
const visible = sortedNonRepeaterContacts.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) =>
|
||||
c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
},
|
||||
[sortedNonRepeaterContacts, query, isContactBlocked]
|
||||
);
|
||||
const filteredNonRepeaterContacts = useMemo(() => {
|
||||
const visible = sortedNonRepeaterContacts.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) => c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
}, [sortedNonRepeaterContacts, query, isContactBlocked]);
|
||||
|
||||
const filteredRooms = useMemo(
|
||||
() => {
|
||||
const visible = sortedRooms.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) =>
|
||||
c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
},
|
||||
[sortedRooms, query, isContactBlocked]
|
||||
);
|
||||
const filteredRooms = useMemo(() => {
|
||||
const visible = sortedRooms.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) => c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
}, [sortedRooms, query, isContactBlocked]);
|
||||
|
||||
const filteredRepeaters = useMemo(
|
||||
() => {
|
||||
const visible = sortedRepeaters.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) =>
|
||||
c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
},
|
||||
[sortedRepeaters, query, isContactBlocked]
|
||||
);
|
||||
const filteredRepeaters = useMemo(() => {
|
||||
const visible = sortedRepeaters.filter((c) => !isContactBlocked(c));
|
||||
return query
|
||||
? visible.filter(
|
||||
(c) => c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
|
||||
)
|
||||
: visible;
|
||||
}, [sortedRepeaters, query, isContactBlocked]);
|
||||
|
||||
// Expand sections while searching; restore prior collapse state when search ends.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,12 +33,14 @@ export function SettingsDatabaseSection({
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
const [purgingDecryptedRaw, setPurgingDecryptedRaw] = useState(false);
|
||||
const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false);
|
||||
const [discoveryBlockedTypes, setDiscoveryBlockedTypes] = useState<number[]>([]);
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||
setDiscoveryBlockedTypes(appSettings.discovery_blocked_types ?? []);
|
||||
}, [appSettings]);
|
||||
|
||||
const handleCleanup = async () => {
|
||||
@@ -92,7 +94,15 @@ export function SettingsDatabaseSection({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await onSaveAppSettings({ auto_decrypt_dm_on_advert: autoDecryptOnAdvert });
|
||||
const update: AppSettingsUpdate = { auto_decrypt_dm_on_advert: autoDecryptOnAdvert };
|
||||
const currentBlocked = appSettings.discovery_blocked_types ?? [];
|
||||
if (
|
||||
discoveryBlockedTypes.length !== currentBlocked.length ||
|
||||
discoveryBlockedTypes.some((t) => !currentBlocked.includes(t))
|
||||
) {
|
||||
update.discovery_blocked_types = discoveryBlockedTypes;
|
||||
}
|
||||
await onSaveAppSettings(update);
|
||||
toast.success('Database settings saved');
|
||||
} catch (err) {
|
||||
console.error('Failed to save database settings:', err);
|
||||
@@ -268,6 +278,54 @@ export function SettingsDatabaseSection({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>Block Discovery of New Node Types</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Checked types will be ignored when heard via advertisement. Existing contacts of these
|
||||
types are still updated. This does not affect contacts added manually or via DM.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{(
|
||||
[
|
||||
[1, 'Block clients'],
|
||||
[2, 'Block repeaters'],
|
||||
[3, 'Block room servers'],
|
||||
[4, 'Block sensors'],
|
||||
] as const
|
||||
).map(([typeCode, label]) => {
|
||||
const checked = discoveryBlockedTypes.includes(typeCode);
|
||||
return (
|
||||
<label key={typeCode} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setDiscoveryBlockedTypes((prev) =>
|
||||
checked ? prev.filter((t) => t !== typeCode) : [...prev, typeCode]
|
||||
)
|
||||
}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{discoveryBlockedTypes.length > 0 && (
|
||||
<p className="text-xs text-warning">
|
||||
New{' '}
|
||||
{discoveryBlockedTypes
|
||||
.map((t) =>
|
||||
t === 1 ? 'clients' : t === 2 ? 'repeaters' : t === 3 ? 'room servers' : 'sensors'
|
||||
)
|
||||
.join(', ')}{' '}
|
||||
heard via advertisement will not be added to your contact list.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-destructive" role="alert">
|
||||
{error}
|
||||
|
||||
@@ -69,6 +69,7 @@ const baseSettings: AppSettings = {
|
||||
flood_scope: '',
|
||||
blocked_keys: [],
|
||||
blocked_names: [],
|
||||
discovery_blocked_types: [],
|
||||
};
|
||||
|
||||
function renderModal(overrides?: {
|
||||
|
||||
@@ -332,6 +332,7 @@ export interface AppSettings {
|
||||
flood_scope: string;
|
||||
blocked_keys: string[];
|
||||
blocked_names: string[];
|
||||
discovery_blocked_types: number[];
|
||||
}
|
||||
|
||||
export interface AppSettingsUpdate {
|
||||
@@ -342,6 +343,7 @@ export interface AppSettingsUpdate {
|
||||
flood_scope?: string;
|
||||
blocked_keys?: string[];
|
||||
blocked_names?: string[];
|
||||
discovery_blocked_types?: number[];
|
||||
}
|
||||
|
||||
export interface MigratePreferencesRequest {
|
||||
|
||||
@@ -1247,8 +1247,8 @@ class TestMigration039:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 9
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 10
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
@@ -1319,8 +1319,8 @@ class TestMigration039:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 9
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 10
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
@@ -1386,8 +1386,8 @@ class TestMigration039:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 3
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 4
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
@@ -1439,8 +1439,8 @@ class TestMigration040:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 8
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 9
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
@@ -1501,8 +1501,8 @@ class TestMigration041:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 7
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 8
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
@@ -1554,8 +1554,8 @@ class TestMigration042:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 6
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 7
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
@@ -1694,8 +1694,8 @@ class TestMigration046:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 2
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 3
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
@@ -1788,8 +1788,8 @@ class TestMigration047:
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 1
|
||||
assert await get_version(conn) == 47
|
||||
assert applied == 2
|
||||
assert await get_version(conn) == 48
|
||||
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
|
||||
@@ -630,6 +630,7 @@ class TestAppSettingsRepository:
|
||||
"flood_scope": "",
|
||||
"blocked_keys": "[]",
|
||||
"blocked_names": "[]",
|
||||
"discovery_blocked_types": "[]",
|
||||
}
|
||||
)
|
||||
mock_conn.execute = AsyncMock(return_value=mock_cursor)
|
||||
|
||||
Reference in New Issue
Block a user