mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-23 22:32:29 +00:00
feat(config, docs): enhance rain command and stats collection configuration
- Updated the `[Rain_Command]` section in `config.ini.example` to include support for snow alongside rain, improving the command's functionality. - Enhanced documentation for the rain command to reflect the new snow alias and clarify response behavior based on the selected keyword. - Added a new `collect_stats` option in the `[Stats_Command]` section, allowing stats collection to be enabled independently of the user-facing command, with updated documentation to explain its behavior. - Improved the web viewer documentation to clarify how stats are collected and displayed, ensuring users understand the configuration options.
This commit is contained in:
1 parent
b8decd8d66
commit
b59fa9cec9
10 files changed
+163
-37
No files matched your search
+9
-2
@@ -778,7 +778,8 @@ enabled = true
|
||||
|
||||
[Rain_Command]
|
||||
# Minute-level rain nowcast ("rain starting in ~20min") via Open-Meteo's
|
||||
# 15-minutely precipitation. Worldwide, no API key. Keywords: rain, nowcast.
|
||||
# 15-minutely precipitation. Worldwide, no API key. Keywords: rain, nowcast, snow.
|
||||
# (snow leads the reply with snowfall depth; rain/nowcast lead with rain amount.)
|
||||
# Usage: rain [city|zipcode|lat,lon] (no location -> companion, then bot location)
|
||||
enabled = true
|
||||
# How far ahead to look, in minutes (default: 120)
|
||||
@@ -875,7 +876,8 @@ api_timeout = 10
|
||||
cache_ttl_minutes = 360
|
||||
|
||||
[Stats_Command]
|
||||
# Enable or disable the stats command (true/false)
|
||||
# Enable or disable the user-facing stats command (true/false)
|
||||
# This only controls whether the bot responds to the "stats" command.
|
||||
enabled = true
|
||||
|
||||
# Data retention settings
|
||||
@@ -889,6 +891,11 @@ data_retention_days = 7
|
||||
auto_cleanup = true
|
||||
|
||||
# Stats collection settings
|
||||
# Collect message, command, and path stats for the web viewer dashboard
|
||||
# true: Populate stats tables even if the stats command is disabled
|
||||
# false: Disable stats table writes
|
||||
collect_stats = true
|
||||
|
||||
# Track all incoming messages (not just commands)
|
||||
# true: Record all messages for comprehensive stats
|
||||
# false: Only record command executions
|
||||
|
||||
@@ -246,12 +246,13 @@ aqi help
|
||||
|
||||
Minute-level rain nowcast — tells you when precipitation is about to **start** or **stop** in the next couple hours, using Open-Meteo's 15-minutely precipitation forecast. Works worldwide with no API key.
|
||||
|
||||
**Aliases:** `nowcast`
|
||||
**Aliases:** `nowcast`, `snow`
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
rain [city|zipcode|lat,lon]
|
||||
nowcast [city|zipcode|lat,lon]
|
||||
snow [city|zipcode|lat,lon]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
@@ -260,6 +261,7 @@ rain
|
||||
rain seattle
|
||||
rain 98101
|
||||
rain 47.6,-122.3
|
||||
snow denver
|
||||
```
|
||||
|
||||
**Response:** A single line describing the upcoming precipitation, for example:
|
||||
@@ -268,6 +270,8 @@ rain 47.6,-122.3
|
||||
- `🌧️ Heavy rain steady for 2h+ in Seattle` — raining now, no break in the window
|
||||
- `☀️ No rain expected in next 2h for Seattle` — dry through the window
|
||||
|
||||
The keyword sets which precipitation the answer leads with: `rain`/`nowcast` lead with rain (liquid amount), while `snow` leads with snowfall (reported as depth, e.g. `🌨️ Snow starting in ~40min (~1.5 in snow)`). Either keyword still mentions the other type when it's in the window, and snow/ice changeovers are called out. Bare countries or US states (e.g. `rain france`, `snow texas`) default to the region's capital with a heads-up, since one centroid isn't representative.
|
||||
|
||||
When no location is given, uses the sender's companion location if known, then the bot's configured location.
|
||||
|
||||
**Configuration:** `[Rain_Command]` — `enabled`, `window_minutes` (how far ahead to look, default 120), `precip_threshold_mm` (sensitivity, default 0.1), and optional `default_lat`/`default_lon`. Temperature/precipitation source units are shared via `[Weather]` (`weather_model` is honored).
|
||||
@@ -684,6 +688,29 @@ sports mlb
|
||||
|
||||
**Response:** Current scores and game information for the requested teams or league.
|
||||
|
||||
> World Cup scores are also available year-round here, e.g. `sports fifa`.
|
||||
|
||||
---
|
||||
|
||||
### `wc` or `worldcup`
|
||||
|
||||
FIFA World Cup scores and schedule. Responds **only while a World Cup (men's or women's) is actually in progress** — the active tournament is auto-detected from the ESPN schedule, so no dates need to be configured. Outside a tournament it replies that none is in progress.
|
||||
|
||||
**Usage:**
|
||||
- `wc` - Live/most recent scores and upcoming fixtures
|
||||
- `wc <nation>` - Focus on a specific nation's match
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
wc
|
||||
worldcup
|
||||
wc argentina
|
||||
```
|
||||
|
||||
**Response:** Current scores, match state, and upcoming fixtures for the active tournament.
|
||||
|
||||
**Configuration:** `[Worldcup_Command]` — `enabled`, `api_timeout`, `cache_ttl_minutes`. For proactive live match announcements posted to a channel, see the [World Cup Live Service](service-plugins.md).
|
||||
|
||||
---
|
||||
|
||||
## MeshCore Utility Commands
|
||||
|
||||
@@ -15,7 +15,7 @@ purging_log_retention_days = 90
|
||||
mesh_connections_retention_days = 7
|
||||
```
|
||||
|
||||
Stats tables (message_stats, command_stats, path_stats) use **`[Stats_Command]`** `data_retention_days` (default 7); the scheduler runs that cleanup daily as well. Stats are **collected** when the stats command is enabled, or when the optional **`collect_stats = true`** is set under `[Stats_Command]` (so the web viewer dashboard can show message/command/path stats even if the `stats` chat command is disabled).
|
||||
Stats tables (message_stats, command_stats, path_stats) use **`[Stats_Command]`** `data_retention_days` (default 7); the scheduler runs that cleanup daily as well. Stats are **collected** by default with **`collect_stats = true`** under `[Stats_Command]`, even if the user-facing `stats` chat command is disabled with `enabled = false`. Set **`collect_stats = false`** only if you want to stop writing those dashboard stats tables.
|
||||
|
||||
## Tables and defaults
|
||||
|
||||
|
||||
@@ -40,3 +40,4 @@ Documentation for the MeshCore bot: setup, configuration, commands, and services
|
||||
| [Map Uploader](map-uploader.md) | Uploading to map.meshcore.dev |
|
||||
| [Weather Service](weather-service.md) | Scheduled weather and alerts |
|
||||
| [Repeater Prefix Collision Service](repeater-prefix-collision-service.md) | Detect repeater prefix collisions |
|
||||
| [World Cup](worldcup.md) | World Cup command and live match announcements |
|
||||
@@ -13,6 +13,7 @@ Service plugins extend the bot with background services that run alongside the m
|
||||
| Webhook Service (`Webhook`) | Accept inbound HTTP POST payloads and relay to mesh channels or DMs |
|
||||
| [Earthquake Service](earthquake-service.md) | Earthquake alerts for a configured region (USGS API, defaults: California) |
|
||||
| [Repeater Prefix Collision Service](repeater-prefix-collision-service.md) | Alerts when a newly heard repeater prefix collides with an existing repeater prefix |
|
||||
| [World Cup Live Service](worldcup.md) | Proactive FIFA World Cup match announcements (auto-detects the active tournament) |
|
||||
|
||||
## Enabling a plugin
|
||||
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ curl http://localhost:5000/api/stats
|
||||
|
||||
The viewer uses the same database as the bot by default (`[Bot] db_path`, typically `meshcore_bot.db`). That single file holds repeater contacts, mesh graph, packet stream, and other data so the viewer can show everything.
|
||||
|
||||
**Dashboard stats** (message/command counts, top users, etc.) come from the stats tables (`message_stats`, `command_stats`, `path_stats`). To populate these when the `stats` chat command is disabled, you can set the optional config under `[Stats_Command]`: `collect_stats = true`.
|
||||
**Dashboard stats** (message/command counts, top users, etc.) come from the stats tables (`message_stats`, `command_stats`, `path_stats`). Stats collection is enabled by default with `[Stats_Command] collect_stats = true`, even if the user-facing `stats` chat command is disabled with `enabled = false`. Set `collect_stats = false` only if you want to stop writing those dashboard stats tables.
|
||||
|
||||
## Migrating from a separate web viewer database
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# World Cup
|
||||
|
||||
The bot has two World Cup features that work together, both driven by ESPN data and active **only while a FIFA World Cup (men's or women's) is actually in progress**. The active tournament is auto-detected from the ESPN schedule, so no dates need to be configured — outside a tournament both features idle.
|
||||
|
||||
- **`wc` / `worldcup` command** — on-demand scores and fixtures (see the [Command Reference](command-reference.md#wc-or-worldcup)).
|
||||
- **World Cup Live Service** — proactive match announcements posted to a channel.
|
||||
|
||||
World Cup scores are also available year-round through the regular [`sports` command](command-reference.md#sports), e.g. `sports fifa`.
|
||||
|
||||
## Command (`[Worldcup_Command]`)
|
||||
|
||||
```ini
|
||||
[Worldcup_Command]
|
||||
# Enable or disable the World Cup command (true/false). Commands: wc / worldcup
|
||||
enabled = true
|
||||
|
||||
# ESPN API timeout in seconds
|
||||
api_timeout = 10
|
||||
|
||||
# How long (minutes) to cache season detection and nation roster lookups,
|
||||
# to avoid repeated ESPN requests. Default: 360 (6 hours).
|
||||
cache_ttl_minutes = 360
|
||||
```
|
||||
|
||||
## Live Service (`[Worldcup_Service]`)
|
||||
|
||||
Posts proactive messages to a channel as matches progress, for example:
|
||||
|
||||
```
|
||||
Group E: Côte d'Ivoire 0, Ecuador 0 (half-time)
|
||||
Group J: Argentina 1, Algeria 0 — 23' Lionel Messi
|
||||
```
|
||||
|
||||
The service only runs while a tournament is in progress and idles otherwise. It pairs with the `wc`/`worldcup` command. Enable it in `[Worldcup_Service]`:
|
||||
|
||||
```ini
|
||||
[Worldcup_Service]
|
||||
enabled = false
|
||||
|
||||
# Optional regional TC_FLOOD scope for mesh channel posts from this service.
|
||||
# flood_scope = #west
|
||||
|
||||
# Channel to post live updates to
|
||||
channel = #general
|
||||
|
||||
# Poll interval (ms) while a tournament is active but no match is in progress. Default: 60000
|
||||
poll_interval = 60000
|
||||
|
||||
# Faster poll interval (ms) used while at least one match is LIVE. ESPN's scoreboard is
|
||||
# edge-cached ~15-20s, so polling faster yields no fresher data. Default: 20000
|
||||
live_poll_interval = 20000
|
||||
|
||||
# Idle interval (seconds) used when no tournament is in progress. Default: 1800 (30 min)
|
||||
idle_interval = 1800
|
||||
|
||||
# Which match events to announce (each true/false)
|
||||
announce_kickoff = true
|
||||
announce_goals = true
|
||||
# Post a follow-up when a previously-announced goal is overturned by VAR. Requires announce_goals.
|
||||
announce_disallowed = true
|
||||
# Announce red cards
|
||||
announce_red_cards = true
|
||||
# Announce yellow cards too. OFF by default — yellows are frequent and can flood a mesh channel.
|
||||
announce_yellow_cards = false
|
||||
```
|
||||
|
||||
See `config.ini.example` for the full annotated list of options.
|
||||
@@ -47,11 +47,11 @@ class StatsCommand(BaseCommand):
|
||||
self.stats_enabled = self.get_config_value('Stats_Command', 'enabled', fallback=None, value_type='bool')
|
||||
if self.stats_enabled is None:
|
||||
self.stats_enabled = self.get_config_value('Stats_Command', 'stats_enabled', fallback=True, value_type='bool')
|
||||
# Optional: collect_stats (defaults to stats_enabled). When true, messages/commands/paths
|
||||
# are recorded for the web viewer dashboard even if enabled = false.
|
||||
# Optional: collect_stats controls dashboard/stat table writes independently
|
||||
# of whether the user-facing stats command is enabled.
|
||||
self.collect_stats = self.get_config_value('Stats_Command', 'collect_stats', fallback=None, value_type='bool')
|
||||
if self.collect_stats is None:
|
||||
self.collect_stats = self.stats_enabled
|
||||
self.collect_stats = True
|
||||
self.data_retention_days = self.get_config_value('Stats_Command', 'data_retention_days', fallback=7, value_type='int')
|
||||
self.auto_cleanup = self.get_config_value('Stats_Command', 'auto_cleanup', fallback=True, value_type='bool')
|
||||
self.track_all_messages = self.get_config_value('Stats_Command', 'track_all_messages', fallback=True, value_type='bool')
|
||||
|
||||
+24
-24
@@ -4160,6 +4160,30 @@ class BotDataViewer:
|
||||
cursor.execute(query)
|
||||
stats['top_users'] = [{'user': row[0], 'count': row[1]} for row in cursor.fetchall()]
|
||||
|
||||
# Top channels by message count - filter by time window
|
||||
if top_channels_window == '24h':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-24 hours')"
|
||||
elif top_channels_window == '7d':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-7 days')"
|
||||
elif top_channels_window == '30d':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-30 days')"
|
||||
else: # 'all'
|
||||
time_filter = ""
|
||||
|
||||
query = f"""
|
||||
SELECT channel, COUNT(*) as message_count, COUNT(DISTINCT sender_id) as unique_users
|
||||
FROM message_stats
|
||||
WHERE channel IS NOT NULL {time_filter}
|
||||
GROUP BY channel
|
||||
ORDER BY message_count DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
cursor.execute(query)
|
||||
stats['top_channels'] = [
|
||||
{'channel': row[0], 'messages': row[1], 'users': row[2]}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
if 'command_stats' in tables:
|
||||
cursor.execute("SELECT COUNT(*) FROM command_stats")
|
||||
stats['total_commands'] = cursor.fetchone()[0]
|
||||
@@ -4240,30 +4264,6 @@ class BotDataViewer:
|
||||
else:
|
||||
stats['bot_reply_rate_30d'] = 0
|
||||
|
||||
# Top channels by message count - filter by time window
|
||||
if top_channels_window == '24h':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-24 hours')"
|
||||
elif top_channels_window == '7d':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-7 days')"
|
||||
elif top_channels_window == '30d':
|
||||
time_filter = "AND timestamp > strftime('%s', 'now', '-30 days')"
|
||||
else: # 'all'
|
||||
time_filter = ""
|
||||
|
||||
query = f"""
|
||||
SELECT channel, COUNT(*) as message_count, COUNT(DISTINCT sender_id) as unique_users
|
||||
FROM message_stats
|
||||
WHERE channel IS NOT NULL {time_filter}
|
||||
GROUP BY channel
|
||||
ORDER BY message_count DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
cursor.execute(query)
|
||||
stats['top_channels'] = [
|
||||
{'channel': row[0], 'messages': row[1], 'users': row[2]}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
# Path statistics (if path_stats table exists)
|
||||
if 'path_stats' in tables:
|
||||
cursor.execute("""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import configparser
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -45,7 +45,7 @@ def _make_db_manager():
|
||||
return db
|
||||
|
||||
|
||||
def _make_bot(enabled=True):
|
||||
def _make_bot(enabled=True, collect_stats=True):
|
||||
bot = MagicMock()
|
||||
bot.logger = Mock()
|
||||
config = configparser.ConfigParser()
|
||||
@@ -57,7 +57,8 @@ def _make_bot(enabled=True):
|
||||
config.add_section("Keywords")
|
||||
config.add_section("Stats_Command")
|
||||
config.set("Stats_Command", "enabled", str(enabled).lower())
|
||||
config.set("Stats_Command", "collect_stats", "true")
|
||||
if collect_stats is not None:
|
||||
config.set("Stats_Command", "collect_stats", str(collect_stats).lower())
|
||||
bot.config = config
|
||||
bot.translator = MagicMock()
|
||||
bot.translator.translate = Mock(side_effect=lambda key, **kw: key)
|
||||
@@ -141,6 +142,18 @@ class TestStatsCommandEnabled:
|
||||
cmd = StatsCommand(bot)
|
||||
assert cmd.stats_enabled is False
|
||||
|
||||
def test_collect_stats_defaults_true_when_command_disabled(self):
|
||||
bot = _make_bot(enabled=False, collect_stats=None)
|
||||
cmd = StatsCommand(bot)
|
||||
assert cmd.stats_enabled is False
|
||||
assert cmd.collect_stats is True
|
||||
|
||||
def test_collect_stats_explicit_false_opt_out(self):
|
||||
bot = _make_bot(enabled=True, collect_stats=False)
|
||||
cmd = StatsCommand(bot)
|
||||
assert cmd.stats_enabled is True
|
||||
assert cmd.collect_stats is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# record_message
|
||||
@@ -168,6 +181,15 @@ class TestRecordMessage:
|
||||
# Should return early without error
|
||||
cmd.record_message(msg)
|
||||
|
||||
def test_record_message_explicit_collect_stats_false_does_not_insert(self):
|
||||
bot = _make_bot(enabled=True, collect_stats=False)
|
||||
cmd = StatsCommand(bot)
|
||||
msg = mock_message(content="hello", channel="general")
|
||||
cmd.record_message(msg)
|
||||
with bot.db_manager.connection() as conn:
|
||||
count = conn.execute("SELECT COUNT(*) FROM message_stats").fetchone()[0]
|
||||
assert count == 0
|
||||
|
||||
def test_record_message_disabled_track_all(self):
|
||||
bot = _make_bot(enabled=True)
|
||||
cmd = StatsCommand(bot)
|
||||
@@ -298,12 +320,13 @@ class TestRecordPathStats:
|
||||
class TestExecuteStats:
|
||||
def test_execute_disabled_returns_false(self):
|
||||
import asyncio
|
||||
bot = _make_bot(enabled=False)
|
||||
bot = _make_bot(enabled=False, collect_stats=None)
|
||||
cmd = StatsCommand(bot)
|
||||
cmd.stats_enabled = False
|
||||
cmd.send_response = AsyncMock(return_value=True)
|
||||
msg = mock_message(content="stats", channel="general")
|
||||
result = asyncio.run(cmd.execute(msg))
|
||||
assert result is False
|
||||
cmd.send_response.assert_awaited_once()
|
||||
|
||||
def test_execute_enabled_returns_true(self):
|
||||
import asyncio
|
||||
|
||||
Reference in new issue
Block a user