-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtelegram_bot.py
More file actions
241 lines (194 loc) · 9.39 KB
/
Copy pathtelegram_bot.py
File metadata and controls
241 lines (194 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"""
NanoAnalytics — Telegram Bot
============================
Required environment variables:
TELEGRAM_BOT_TOKEN — from @BotFather
ANALYTICS_URL — your NanoAnalytics instance URL, e.g. https://abc.railway.app
ANALYTICS_API_TOKEN — your API_TOKEN env var value from the hosting platform
ANALYTICS_SITE — default site to query, e.g. mysite.com
Install dependencies:
pip install "nano-analytics[bots]"
# or: pip install python-telegram-bot httpx
Run:
python bots/telegram_bot.py
"""
import asyncio
import os
from datetime import datetime, timedelta, timezone
import httpx
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
# ── Config ──────────────────────────────────────────────────────────────────
ANALYTICS_URL = os.environ["ANALYTICS_URL"].rstrip("/")
ANALYTICS_TOKEN = os.environ["ANALYTICS_API_TOKEN"]
DEFAULT_SITE = os.environ.get("ANALYTICS_SITE", "")
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {ANALYTICS_TOKEN}"}
# ── API helper ───────────────────────────────────────────────────────────────
def _range_7d():
now = int(datetime.now(timezone.utc).timestamp())
start = int((datetime.now(timezone.utc) - timedelta(days=7)).timestamp())
return start, now
async def fetch(path: str, **params) -> dict | list:
params.setdefault("site", DEFAULT_SITE)
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(f"{ANALYTICS_URL}{path}", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
def _fmt(n: int) -> str:
return f"{n:,}"
# ── Command handlers ─────────────────────────────────────────────────────────
async def cmd_start(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"📊 *NanoAnalytics Bot*\n\n"
"Available commands:\n"
"/stats — Pageviews & sessions (last 7 days)\n"
"/pages — Top 10 pages\n"
"/referrers — Top traffic sources\n"
"/devices — Device breakdown\n"
"/trend — Daily traffic (last 7 days)\n"
"/languages — Top browser languages\n"
"/countries — Top countries\n"
"/active — Active visitors right now\n"
"/entrypages — Top entry pages\n"
"/peakhours — Busiest hours of the day\n"
"/bouncerates — Bounce rate by page",
parse_mode="Markdown",
)
async def cmd_stats(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
data = await fetch("/api/pageviews", start=start, end=end)
await update.message.reply_text(
f"📈 *Last 7 days — {DEFAULT_SITE}*\n\n"
f"👁 Page Views: `{_fmt(data['views'])}`\n"
f"👤 Sessions: `{_fmt(data['sessions'])}`",
parse_mode="Markdown",
)
async def cmd_pages(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/pages", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("No page data yet.")
return
lines = [f"`{r['path']}` — {_fmt(r['views'])} views" for r in rows]
await update.message.reply_text(
f"📄 *Top Pages — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_referrers(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/referrers", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("No referrer data yet.")
return
lines = [f"`{r['ref']}` — {_fmt(r['views'])}" for r in rows]
await update.message.reply_text(
f"🔗 *Top Referrers — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_devices(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
data = await fetch("/api/devices", start=start, end=end)
total = sum(data.values()) or 1
lines = [
f"🖥 Desktop: `{_fmt(data['desktop'])}` ({data['desktop']*100//total}%)",
f"📱 Mobile: `{_fmt(data['mobile'])}` ({data['mobile']*100//total}%)",
f"📟 Tablet: `{_fmt(data['tablet'])}` ({data['tablet']*100//total}%)",
]
await update.message.reply_text(
f"📱 *Devices — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_trend(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/timeseries", start=start, end=end)
if not rows:
await update.message.reply_text("No trend data yet.")
return
lines = [f"`{r['day']}` — {_fmt(r['views'])} views" for r in rows]
await update.message.reply_text(
f"📅 *Daily Trend — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_languages(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/languages", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("No language data yet.")
return
lines = [f"`{r['lang']}` — {_fmt(r['views'])}" for r in rows]
await update.message.reply_text(
f"🌐 *Top Languages — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_countries(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/countries", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("No country data yet.")
return
lines = [f"`{r['country']}` — {_fmt(r['views'])}" for r in rows]
await update.message.reply_text(
f"🌍 *Top Countries — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_active(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
data = await fetch("/api/active")
lines = [f"`{r['country']}` — {r['sessions']} session(s)" for r in (data.get("countries") or [])]
body = ("\n".join(lines) or "No country breakdown available.") + f"\n\n🟢 *{data['active']} active* (last {data['window_seconds']//60} min)"
await update.message.reply_text(
f"🟢 *Active Visitors — {DEFAULT_SITE}*\n\n" + body,
parse_mode="Markdown",
)
async def cmd_entry_pages(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/entry-pages", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("No entry page data yet.")
return
lines = [f"`{r['path']}` — {_fmt(r['entries'])} entries" for r in rows]
await update.message.reply_text(
f"🚪 *Entry Pages — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_peak_hours(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/peak-hours", start=start, end=end)
if not rows:
await update.message.reply_text("No hour data yet.")
return
lines = [f"`{r['hour']:02d}:00` — {_fmt(r['views'])} views" for r in rows]
await update.message.reply_text(
f"⏰ *Peak Hours — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
async def cmd_bounce_rates(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
start, end = _range_7d()
rows = await fetch("/api/bounce-rates", start=start, end=end, limit=10)
if not rows:
await update.message.reply_text("Not enough data yet.")
return
lines = [f"`{r['path']}` — {r['bounce_rate']}%" for r in rows]
await update.message.reply_text(
f"↩️ *Bounce Rates — {DEFAULT_SITE}*\n\n" + "\n".join(lines),
parse_mode="Markdown",
)
# ── Main ─────────────────────────────────────────────────────────────────────
def main() -> None:
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("stats", cmd_stats))
app.add_handler(CommandHandler("pages", cmd_pages))
app.add_handler(CommandHandler("referrers", cmd_referrers))
app.add_handler(CommandHandler("devices", cmd_devices))
app.add_handler(CommandHandler("trend", cmd_trend))
app.add_handler(CommandHandler("languages", cmd_languages))
app.add_handler(CommandHandler("countries", cmd_countries))
app.add_handler(CommandHandler("active", cmd_active))
app.add_handler(CommandHandler("entrypages", cmd_entry_pages))
app.add_handler(CommandHandler("peakhours", cmd_peak_hours))
app.add_handler(CommandHandler("bouncerates", cmd_bounce_rates))
print(f"🤖 NanoAnalytics Telegram bot started (site: {DEFAULT_SITE})")
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()