Files
meshcore-bot/pyproject.toml
T
agessaman bb3bfe321b feat(region): warn senders whose channel messages carry no region code
Closes #279.

A MeshCore client with no region configured sends every channel message as
a plain FLOOD, which every repeater on the mesh rebroadcasts. This adds two
things: free observation of how much of that the bot hears, and an opt-in
warning to the senders.

Observation classifies each channel message as scoped, global or unknown and
tallies it per channel per local day. It costs one upsert and no airtime, and
it is what lets an operator see the size of the problem before deciding to
spend airtime on it. The classification runs ahead of the flood_scopes
allowlist, because an unscoped message is exactly what that allowlist drops.

Warnings only ever fire on positive RF evidence of an unscoped FLOOD. Absence
of correlation is not proof that a sender omitted a region, so it classifies
as unknown and stays quiet. They are off by default, start in dry run, and are
fenced by min_unscoped_messages, a per-sender cooldown, a mesh-wide cooldown
and a daily cap on attempts. Dry run consumes the same budget it previews, so
the log is what going live would put on the air, not an upper bound. All three
limits read from the database, so a restart cannot release a burst.

Channel-delivered warnings go out at global scope on purpose: the recipient is
by definition outside any region the bot replies under.

New Settings -> Region Warnings page in the web viewer covers all of it. Its
dark-mode striped rows exposed a pre-existing base.html bug where Bootstrap's
light-theme text color survived on a dark row background (~1.3:1); fixed there
for every table in the app.
2026-09-15 22:34:46 -07:00

214 lines
7.5 KiB
TOML

# pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "meshcore-bot"
version = "1.1.0"
description = "MeshCore Bot with commands for mesh testing, utilities, and service integration."
readme = "README.md"
# Table form rather than the SPDX string form: the string form needs
# setuptools>=77, above this project's declared build requirement.
license = { text = "MIT" }
authors = [{ name = "Adam Gessaman", email = "adam@gessaman.com" }]
requires-python = ">=3.10"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
dependencies = [
"pyserial>=3.5",
"bleak>=0.20.0",
"asyncio-mqtt>=0.11.0",
"configparser>=5.3.0",
"python-dateutil>=2.8.2",
"apscheduler>=3.10.0",
"colorlog>=6.7.0",
"requests>=2.31.0",
"urllib3>=2.0.0",
"pyephem>=4.1.4",
"geopy>=2.3.0",
"maidenhead>=1.4.0",
"pytz>=2023.3",
"aiohttp>=3.8.0",
# 2.3.8+ required for zero-hop neighbour discovery (see requirements.txt)
"meshcore>=2.3.8",
"openmeteo-requests>=1.7.2",
"requests-cache>=1.1.1",
"retry-requests>=1.0.0",
"flask>=2.3.0",
"flask-socketio>=5.3.0",
"flask-compress>=1.14",
# Still required: `next_cmd` backs contact and channel operations that have no
# equivalent in the meshcore library yet (see requirements.txt).
"meshcore-cli>=1.4.0",
"feedparser>=6.0.10",
"paho-mqtt>=1.6.0",
"cryptography>=41.0.0",
"pynacl>=1.5.0",
"aiosqlite>=0.19.0",
]
[project.optional-dependencies]
profanity = ["better-profanity>=0.7.0", "unidecode>=1.3.0"]
geo = ["pycountry>=23.12.0", "us>=2.0.0"]
lang = ["langdetect>=1.0.9"]
test = ["pytest>=7.0", "pytest-asyncio>=0.21", "pytest-mock>=3.10", "pytest-cov>=4.0", "pytest-timeout>=2.1.0", "types-requests>=2.31"]
docs = ["mkdocs-material>=9.0.0", "mkdocs-exclude>=1.0.0"]
[project.scripts]
meshcore-bot = "meshcore_bot:main"
meshcore-viewer = "modules.web_viewer.app:main"
[tool.setuptools]
py-modules = ["meshcore_bot"]
include-package-data = false
license-files = ["LICENSE"]
[tool.setuptools.packages.find]
where = ["."]
include = ["modules*", "translations*", "data*"]
exclude = ["tests*", "local*"]
[tool.setuptools.package-data]
modules = [
"web_viewer/templates/*.html",
"web_viewer/templates/**/*.html",
"web_viewer/static/*",
"web_viewer/static/*/*",
"web_viewer/static/*/*/*",
]
translations = ["*.json"]
"data.randomlines" = ["*.txt"]
[tool.ruff]
# Match CI/Makefile installs so lint rules do not drift on unpinned `pip install ruff`.
required-version = "==0.15.15"
line-length = 120
target-version = "py310"
exclude = [".venv", "build", "dist"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
ignore = [
"E501", # line too long (handled by formatter)
# Legacy-code tolerances — pervasive in pre-typed source, not worth a bulk churn:
"E701", # multiple-statements-on-one-line-colon
"E702", # multiple-statements-on-one-line-semicolon
"E711", # comparison-to-none (== None common in legacy)
"E712", # comparison-to-true (== True/False common in legacy)
"E722", # bare-except (used extensively in graceful-degradation patterns)
"E741", # ambiguous-variable-name (l, O, I in legacy loops)
"E402", # module-level import not at top (conditional imports pattern)
"F601", # multi-value-repeated-key-literal (legacy dict pattern)
"B007", # unused-loop-control-variable (rename to _ is a bulk churn)
"B023", # function-uses-loop-variable (legacy closures in packet capture)
"B904", # raise-without-from-in-except (legacy raise pattern)
"C408", # unnecessary-collection-call
"C414", # unnecessary-double-cast-or-process
# pyupgrade rules that would require a bulk typing rewrite — deferred to a code-quality PR:
"UP007", # non-pep604-annotation-union (Union[X, Y] -> X | Y)
"UP035", # deprecated-import (typing.List/Dict/... remain prevalent)
"UP041", # timeout-error-alias (socket.timeout -> TimeoutError)
"UP045", # non-pep604-annotation-optional (Optional[X] -> X | None)
# Simplification suggestions — stylistic, not enforced on legacy code:
"SIM102", # collapsible-if
"SIM103", # needless-bool
"SIM105", # suppressible-exception
"SIM108", # if-else-block-instead-of-if-exp (ternary preference is subjective)
"SIM109", # compare-with-tuple
"SIM110", # reimplemented-builtin
"SIM114", # if-with-same-arms
"SIM115", # open-file-with-context-handler
"SIM117", # multiple-with-statements
"SIM210", # if-expr-with-true-false
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
# ---------------------------------------------------------------------------
# mypy — incremental strict mode
# ---------------------------------------------------------------------------
# Global baseline: safe non-breaking options.
# Per-module overrides below tighten settings for fully-typed modules.
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true
warn_unused_ignores = true
warn_return_any = false # too noisy until all modules are annotated
warn_unused_configs = true
no_implicit_optional = true
strict_optional = true
# New modules written with full type annotations get strict treatment.
[[tool.mypy.overrides]]
module = [
"modules.alert_format",
"modules.commands.schedule_command",
"modules.service_plugins.webhook_service",
"modules.service_plugins.base_service",
"modules.message_handler",
"modules.utils",
"modules.plugin_loader",
"modules.security_utils",
"modules.commands.base_command",
"modules.commands.rain_command",
"modules.region_warning",
]
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
# Modules with known type errors not yet brought up to strict standard.
# Suppress all mypy errors for these modules until they are fully annotated.
[[tool.mypy.overrides]]
module = [
"modules.core",
"modules.command_manager",
"modules.feed_manager",
"modules.mesh_graph",
"modules.scheduler",
"modules.maintenance",
"modules.transmission_tracker",
"modules.commands.alert_command",
"modules.commands.announcements_command",
"modules.commands.aurora_command",
"modules.commands.channels_command",
"modules.commands.greeter_command",
"modules.commands.help_command",
"modules.commands.joke_command",
"modules.commands.multitest_command",
"modules.commands.prefix_command",
"modules.commands.wx_command",
"modules.commands.alternatives.wx_international",
"modules.commands.trace_command",
"modules.commands.solarforecast_command",
"modules.commands.roll_command",
"modules.commands.repeater_command",
"modules.commands.path_command",
"modules.commands.sports_command",
"modules.commands.aqi_command",
"modules.service_plugins.discord_bridge_service",
"modules.service_plugins.packet_capture_service",
"modules.service_plugins.weather_service",
"modules.web_viewer.app",
"modules.web_viewer.integration",
]
ignore_errors = true
# Pytest is configured in pytest.ini, not here. pytest.ini takes precedence over
# pyproject.toml, so a [tool.pytest.ini_options] block in this file would be dead
# config that silently drifts from the settings actually in effect.
[tool.coverage.run]
source = ["modules"]
omit = ["tests/*", ".venv/*"]
[tool.coverage.report]
fail_under = 50
show_missing = true