mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-15 10:39:52 +00:00
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.
214 lines
7.5 KiB
TOML
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
|