This commit is contained in:
2
main.py
2
main.py
@@ -18,7 +18,7 @@ from config import (
|
|||||||
TORRENT_API_PASSWORD,
|
TORRENT_API_PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
||||||
from menus import (
|
from menus.menus import (
|
||||||
main_menu_keyboard,
|
main_menu_keyboard,
|
||||||
torrents_menu_keyboard,
|
torrents_menu_keyboard,
|
||||||
status_menu_keyboard,
|
status_menu_keyboard,
|
||||||
|
|||||||
133
menus.py
133
menus.py
@@ -1,133 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
|
|
||||||
# --- Menu Definitions ---
|
|
||||||
def main_menu_keyboard():
|
|
||||||
return InlineKeyboardMarkup(
|
|
||||||
[
|
|
||||||
[InlineKeyboardButton("Torrents", callback_data="status_downloading")],
|
|
||||||
[InlineKeyboardButton("Status", callback_data="menu_status")],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def torrents_menu_keyboard():
|
|
||||||
return InlineKeyboardMarkup(
|
|
||||||
[
|
|
||||||
# First row: Display the status counts for downloading, paused, seeding
|
|
||||||
[InlineKeyboardButton(f"Downloading", callback_data="status_downloading")],
|
|
||||||
[InlineKeyboardButton(f"Active", callback_data="status_active")],
|
|
||||||
[InlineKeyboardButton(f"All", callback_data="status_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
|
|
||||||
|
|
||||||
|
|
||||||
# --- Callback Query Handler ---
|
|
||||||
async def handle_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
||||||
query = update.callback_query
|
|
||||||
await query.answer()
|
|
||||||
|
|
||||||
match query.data:
|
|
||||||
case "menu_main":
|
|
||||||
await query.edit_message_text(
|
|
||||||
"Choose an option:", reply_markup=main_menu_keyboard()
|
|
||||||
)
|
|
||||||
|
|
||||||
case "status_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 "status_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 "status_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()
|
|
||||||
)
|
|
||||||
156
menus/menus.py
Normal file
156
menus/menus.py
Normal 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
138
menus/portainer.py
Normal 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)
|
||||||
@@ -6,3 +6,5 @@ services:
|
|||||||
container_name: jarvis
|
container_name: jarvis
|
||||||
volumes:
|
volumes:
|
||||||
- /home/portainer/docker-config/local_stuff/jarvis/:/app/config
|
- /home/portainer/docker-config/local_stuff/jarvis/:/app/config
|
||||||
|
env_file:
|
||||||
|
- stack.env
|
||||||
|
|||||||
Reference in New Issue
Block a user