Add portainer menu
All checks were successful
Build and Deploy / build (push) Successful in 2m55s

This commit is contained in:
2025-04-20 17:14:31 +02:00
parent a0833b818b
commit 6e20068410
5 changed files with 297 additions and 134 deletions

156
menus/menus.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
from uptime_kuma_api import MonitorStatus
import api.kuma as kuma
import api.torrent as torrent
import api.ntfy as ntfy
from menus.portainer import button_handler
# --- Menu Definitions ---
def main_menu_keyboard():
return InlineKeyboardMarkup(
[
[InlineKeyboardButton("Torrents", callback_data="torrent_downloading")],
[InlineKeyboardButton("Status", callback_data="menu_status")],
[InlineKeyboardButton("Portainer", callback_data="portainer_menu")],
]
)
def torrents_menu_keyboard():
print("Creating meny keyboard")
return InlineKeyboardMarkup(
[
# First row: Display the status counts for downloading, paused, seeding
[InlineKeyboardButton(f"Downloading", callback_data="torrent_downloading")],
[InlineKeyboardButton(f"Active", callback_data="torrent_active")],
[InlineKeyboardButton(f"All", callback_data="torrent_all")],
# Second row: Back button
[InlineKeyboardButton("🔙 Back", callback_data="menu_main")],
]
)
def status_menu_keyboard():
return InlineKeyboardMarkup(
[[InlineKeyboardButton("🔙 Back", callback_data="menu_main")]]
)
def format_torrents(torrents):
if len(torrents) == 0:
return "No torrents."
text = ""
i = 0
for torrent in torrents:
if i > 5:
text += "...\n"
return text
text += f"Name: {torrent['name']}\n"
text += f"State: {torrent['state']}\n"
text += f"Progress: {torrent['progress']:.2f}%\n"
text += f"ETA: {torrent['eta']}\n"
text += "-" * 20 + "\n"
text += f"- {torrent['name']} - {torrent['progress']} ({torrent['eta']})\n"
i += 1
return text
# ===================================
# --- MAIN HANDLER FUNCTION ---
# ==================================
# --- Callback Query Handler ---
async def handle_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
if query.data.startswith("portainer_"):
await button_handler(update, context)
else:
print(query.data)
match query.data:
case "menu_main":
await query.edit_message_text(
"Choose an option:", reply_markup=main_menu_keyboard()
)
case "torrent_downloading":
t_api = context.bot_data.get("torrent_api", {})
torrents = t_api.get_filtered_torrents("downloading")
if len(torrents) == 0:
text = "No downloading torrents."
else:
text = format_torrents(torrents)
await query.edit_message_text(
text, reply_markup=torrents_menu_keyboard()
)
case "torrent_active":
t_api = context.bot_data.get("torrent_api", {})
torrents = t_api.get_filtered_torrents("active")
if len(torrents) == 0:
text = "No active torrents."
else:
text = format_torrents(torrents)
await query.edit_message_text(
text, reply_markup=torrents_menu_keyboard()
)
case "torrent_all":
t_api = context.bot_data.get("torrent_api", {})
torrents = t_api.get_filtered_torrents("all")
if len(torrents) == 0:
text = "No torrents."
else:
text = format_torrents(torrents)
await query.edit_message_text(
text, reply_markup=torrents_menu_keyboard()
)
case "menu_status":
k_api = context.bot_data.get("kuma_api", {})
monitors = k_api.get_status()
up_text, down_text, paused_text = "", "", ""
for _, monitor in monitors.items():
status = monitor["status"]
if status == MonitorStatus.UP:
up_text += f" - {monitor['name']}\n"
elif status == MonitorStatus.DOWN:
down_text += f" - {monitor['name']}\n"
else:
paused_text += f" - {monitor['name']}\n"
status_text = f"📡 *Status:*\n\n 🟢 Up:\n{up_text}\n🔴 Down\n{down_text}\n⏸️ Paused\n{paused_text}"
await query.edit_message_text(
status_text, reply_markup=status_menu_keyboard()
)
case _:
await query.edit_message_text(
"Unknown option selected.", reply_markup=main_menu_keyboard()
)

138
menus/portainer.py Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
import requests
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
)
from config import BOT_TOKEN, PORTAINER_API_KEY
PORTAINER_URL = "https://192.168.1.17:9443/api"
STACKS_PER_PAGE = 5
user_page = 0 # Tracks current page per user
def get_stacks():
headers = {"X-API-Key": f"{PORTAINER_API_KEY}"}
response = requests.get(
f"{PORTAINER_URL}/stacks?filters=" + '{"EndpointID":2}',
headers=headers,
verify=False,
)
return response.json() if response.ok else []
def get_stack_status(stack_id):
"""Fetch the stack status based on its ID."""
headers = {"X-API-Key": f"{PORTAINER_API_KEY}"}
response = requests.get(
f"{PORTAINER_URL}/stacks/{stack_id}?endpointId=2", headers=headers, verify=False
)
if response.ok:
stack_info = response.json()
return stack_info.get(
"Status", ""
) # Assuming the status is returned in lowercase
return None
async def start_portainer(update: Update, context: ContextTypes.DEFAULT_TYPE):
global user_page
user_page = 0
await send_stack_list(update, context)
def build_stack_markup(stacks, page):
start_idx = page * STACKS_PER_PAGE
end_idx = start_idx + STACKS_PER_PAGE
sliced = stacks[start_idx:end_idx]
keyboard = []
for stack in sliced:
stack_name = stack["Name"]
stack_id = stack["Id"]
status = get_stack_status(stack_id)
# Add smiley based on stack status
if status == 1:
smiley = "🟢" # Stack is up
else:
smiley = "🔴" # Stack is down
# Button labels: Stack name with status smiley
row = [
InlineKeyboardButton(
f"{smiley} {stack_name}",
callback_data=f"portainer_select_{stack_id}",
),
InlineKeyboardButton(
f"▶️ Start", callback_data=f"portainer_start_{stack_id}"
),
InlineKeyboardButton(f"⏹ Stop", callback_data=f"portainer_stop_{stack_id}"),
]
keyboard.append(row)
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton("⬅️ Prev", callback_data="portainer_prev"))
if end_idx < len(stacks):
nav_row.append(InlineKeyboardButton("➡️ Next", callback_data="portainer_next"))
keyboard.append(nav_row)
keyboard.append([InlineKeyboardButton("🔙 Main Menu", callback_data="menu_main")])
return InlineKeyboardMarkup(keyboard)
async def send_stack_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
global user_page
page = user_page
stacks = get_stacks()
if not stacks:
await update.message.reply_text(
"No stacks found or unable to connect to Portainer."
)
return
if update.callback_query:
await update.callback_query.answer()
await update.callback_query.edit_message_text(
text=f"Select a stack (page {page + 1}):",
reply_markup=build_stack_markup(stacks, page),
)
else:
await update.message.reply_text(
text=f"Select a stack (page {page + 1}):",
reply_markup=build_stack_markup(stacks, page),
)
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
global user_page
query = update.callback_query
data = query.data
if data == "portainer_next":
user_page += 1
elif data == "portainer_prev":
user_page = max(0, user_page - 1)
elif data.startswith("portainer_start_") or data.startswith("portainer_stop_"):
_, action, stack_id = data.split("_")
await handle_stack_action(query, action, stack_id)
await send_stack_list(update, context)
async def handle_stack_action(query, action, stack_id):
endpoint = f"{PORTAINER_URL}/stacks/{stack_id}/{action}?endpointId=2"
headers = {"X-API-Key": f"{PORTAINER_API_KEY}"}
response = requests.post(endpoint, headers=headers, verify=False)
msg = "✅ Success!" if response.ok else f"❌ Failed: {response.text}"
await query.answer(text=msg, show_alert=True)