Documentation Index
Fetch the complete documentation index at: https://mintlify.com/karilaa-dev/tt-bot/llms.txt
Use this file to discover all available pages before exploring further.
TT-Bot includes a separate statistics bot that tracks usage metrics, generates graphs, and provides analytics.
Statistics bot
The stats bot runs independently from the main bot:
# From stats.py:18
async def main() -> None:
await setup_db(config['bot']['db_url'])
scheduler.start()
dp.include_routers(
stats_router,
)
bot_info = await bot.get_me()
logging.info(f'{bot_info.full_name} [@{bot_info.username}, id:{bot_info.id}]')
await dp.start_polling(bot)
Run with:
Tracked metrics
The bot tracks:
- Chats - Total users and groups
- Videos - Total downloads and unique users
- Images - Slideshow downloads and unique users
- Music - Audio extractions and unique users
- Timestamps - Registration and download times
Database models
# From stats/misc.py:9
from data.models import Users, Video, Music
Three main tables:
Users - User registrations
Video - Video/image downloads
Music - Audio extractions
Auto-updating statistics
The bot automatically updates statistics messages in a configured channel:
# From stats.py:9
if config["logs"]["stats_chat"] != "0":
# Split message mode - run both immediately
scheduler.add_job(update_overall_stats, misfire_grace_time=None)
scheduler.add_job(update_daily_stats, misfire_grace_time=None)
# Schedule separate updates
scheduler.add_job(update_overall_stats, "interval", hours=1, id='stats_overall', misfire_grace_time=None)
scheduler.add_job(update_daily_stats, "interval", minutes=5, id='stats_daily', misfire_grace_time=None)
Schedule:
- Overall stats: Updated every hour
- Daily stats: Updated every 5 minutes
Configuration
STATS_CHAT=-1001234567890
STATS_MESSAGE_ID=123
DAILY_STATS_MESSAGE_ID=456
The bot edits these message IDs with updated statistics.
Overall statistics
# From stats/misc.py:87
async def get_overall_stats():
result = '<b>📊Overall Stats</b>\n'
result += await bot_stats(chat_type='users', stats_time=0)
result += '\n<b>Groups</b>\n'
result += await bot_stats(chat_type='groups', stats_time=0)
return result
Example output:
📊Overall Stats
Chats: 1234
Music: 567
┗ Unique: 234
Videos: 8901
┗ Unique: 789
┗ Images: 456
┗ Unique: 123
Groups
Chats: 45
Music: 12
┗ Unique: 8
Videos: 234
┗ Unique: 34
┗ Images: 89
┗ Unique: 12
Daily statistics
# From stats/misc.py:95
async def get_daily_stats():
result = '<b>📊Last 24 Hours</b>\n'
result += await bot_stats(chat_type='users', stats_time=86400)
result += '\n<b>Groups</b>\n'
result += await bot_stats(chat_type='groups', stats_time=86400)
return result
Shows activity in the last 24 hours with the same format.
Timestamps
All statistics include formatted timestamps in multiple time zones:
# From stats/misc.py:103
def get_formatted_timestamp():
ts = datetime.fromtimestamp(tCurrent())
prague_time = ts.astimezone(ZoneInfo("Europe/Prague")).strftime("%H:%M:%S / %d %B %Y")
la_time = ts.astimezone(ZoneInfo("America/Los_Angeles")).strftime("%I:%M:%S %p / %d %B %Y")
return f'\n\n<code>🇨🇿 {prague_time}\n🇺🇸 {la_time}</code>'
Example:
🇨🇿 14:32:15 / 04 March 2026
🇺🇸 05:32:15 AM / 04 March 2026
Query builder
The stats system supports filtering by chat type and time period:
# From stats/misc.py:14
async def bot_stats(chat_type='all', stats_time=86400):
async with await get_session() as db:
if stats_time == 0:
period = 0
else:
period = tCurrent() - stats_time
# Build filter conditions
if chat_type == 'all':
user_filter = Users.user_id != 0
video_filter = Video.user_id != 0
music_filter = Music.user_id != 0
elif chat_type == 'groups':
user_filter = Users.user_id < 0
video_filter = Video.user_id < 0
music_filter = Music.user_id < 0
else: # users
user_filter = Users.user_id > 0
video_filter = Video.user_id > 0
music_filter = Music.user_id > 0
Filters:
chat_type='all' - All chats
chat_type='users' - Private chats only (user_id > 0)
chat_type='groups' - Group chats only (user_id < 0)
stats_time=0 - All time
stats_time=86400 - Last 24 hours
Graph generation
The bot generates time-series graphs using matplotlib:
# From stats/graphs.py:17
def create_time_series_plot(
days: List[datetime],
amounts: List[int],
title: str
) -> bytes:
"""Create an optimized time series plot using Matplotlib."""
# Set up the figure and axis
plt.figure(figsize=(12, 6))
fig, ax = plt.subplots(figsize=(12, 6))
if not days or not amounts:
# Display no data message
ax.text(0.5, 0.5, "No data available for this period",
horizontalalignment='center', verticalalignment='center',
transform=ax.transAxes, fontsize=16)
ax.set_title(title, fontsize=18, pad=20)
else:
# Plot the main line
ax.plot(days, amounts, color='#1f77b4', linewidth=2, label='Count')
Graph features
- Time series plots: Line graphs showing trends over time
- Date formatting: X-axis shows dates with automatic interval selection
- Grid: Light gray grid for readability
- Empty state: “No data available” message when no data exists
- High DPI: 100 DPI for crisp rendering
Data processing
Graphs use pandas for efficient data processing:
# From stats/graphs.py:96
def process_time_series_data(
timestamps: List[int],
depth: str,
period: int
) -> Tuple[List[datetime], List[int]]:
"""Process raw timestamps into grouped time series data."""
if not timestamps:
return [], []
# Convert to DataFrame for efficient processing
df = pd.DataFrame({"timestamp": timestamps})
df["datetime"] = pd.to_datetime(df["timestamp"], unit="s")
# Group by specified time depth
df_grouped = df.groupby(df["datetime"].dt.strftime(depth)).size().reset_index()
df_grouped.columns = ["time_str", "count"]
Time depths:
'%Y-%m-%d' - Daily (frequency: ‘D’)
'%Y-%m' - Monthly (frequency: ‘M’)
'%Y' - Yearly (frequency: ‘Y’)
'%Y-%m-%d %H' - Hourly (frequency: ‘H’)
Async graph generation
Graphs are generated in thread pool to avoid blocking:
# From stats/graphs.py:143
async def plot_user_graph(
graph_name: str,
depth: str,
period: int,
id_condition: str,
table_name: str
) -> bytes:
"""Main function to create user activity graphs."""
try:
# Get raw data
timestamps = await get_time_series_data(table_name, period, id_condition)
# Process data
days, amounts = process_time_series_data(timestamps, depth, period)
# Create plot in thread pool for better performance
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: create_time_series_plot(days, amounts, graph_name)
)
Botstat integration
The bot integrates with Botstat.io for external analytics:
# From stats/botstat.py:8
class Botstat:
def __init__(self):
self.token = config['bot']['token']
self.botstat = config['api']['botstat']
self.request_url = f"https://api.botstat.io/create/{self.token}/{self.botstat}"
self.request_params = {
"notify_id": admin_ids[0],
"hide": "false",
"show_file_result": "true"
}
Upload task
# From stats/botstat.py:19
async def start_task(self):
logging.info("Starting botstat task...")
request_file = await get_users_file()
form_data = aiohttp.FormData()
form_data.add_field("file", request_file.data, filename=request_file.filename, content_type="text/plain")
async with aiohttp.ClientSession() as client:
async with client.post(self.request_url,
params=self.request_params, data=form_data) as response:
code = response.status
if code != 200:
raise Exception(f'API error code:{code}')
Uploads user list to Botstat.io for centralized analytics.
Configuration
BOTSTAT_API_KEY=your_botstat_key
Database queries
All statistics use efficient SQLAlchemy queries:
# From stats/misc.py:44
# Get total chats
stmt = select(func.count(Users.user_id)).where(user_filter)
result = await db.execute(stmt)
chats = result.scalar()
# Get total videos
stmt = select(func.count(Video.user_id)).where(video_filter)
result = await db.execute(stmt)
vid = result.scalar()
# Get unique video downloaders
stmt = select(func.count(func.distinct(Video.user_id))).where(video_filter)
result = await db.execute(stmt)
vid_u = result.scalar()
Queries use:
func.count() for totals
func.distinct() for unique users
- Filters for time periods and chat types
Query optimization
- Uses indexed columns (user_id, timestamps)
- Limits data retrieval to required time periods
- Aggregates in database rather than application
Graph rendering
- Runs in thread pool to avoid blocking event loop
- Uses pandas for efficient data processing
- Filters zero values for cleaner plots
- Automatic date interval selection based on data range
Scheduling
- Separate update intervals for overall (1h) and daily (5m) stats
- Misfire grace time disabled for immediate catch-up
- Independent scheduler from main bot
Running the stats bot
Development
Production with Docker
# docker-compose.yml
services:
stats:
build: .
command: uv run stats.py
env_file: .env
depends_on:
- db
Environment setup
# Required
DB_URL=postgresql+asyncpg://user:pass@localhost/ttbot
STATS_BOT_TOKEN=your_stats_bot_token
# Optional (for auto-updates)
STATS_CHAT=-1001234567890
STATS_MESSAGE_ID=123
DAILY_STATS_MESSAGE_ID=456
# Optional (for Botstat.io)
BOTSTAT_API_KEY=your_key