diff --git a/.vscode/launch.json b/.vscode/launch.json
index 26f900b..724dd63 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,14 +5,14 @@
"version": "0.2.0",
"configurations": [
{
- "name": "bot",
+ "name": "Bot",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/bot/app/__main__.py",
"cwd": "${workspaceFolder}/bot/app/",
"console": "integratedTerminal",
- "envFile": "${workspaceFolder}/bot.env",
- "justMyCode": true
+ "justMyCode": true,
+ "envFile": "${workspaceFolder}/bot.env"
}
]
}
\ No newline at end of file
diff --git a/bot.env b/bot.env
index 2be47ef..2c26da4 100644
--- a/bot.env
+++ b/bot.env
@@ -1,5 +1,8 @@
tg_token=""
admin_chat=""
-db_name="adm"
+prometheus_rules_url="http://localhost:9090/api/v1/rules"
+prometheus_alert_groups="maas-rules"
+db_name="maas"
db_user="adm"
db_pass="adm"
+db_port="5432"
\ No newline at end of file
diff --git a/bot/app/__main__.py b/bot/app/__main__.py
index da1d049..a9eace1 100644
--- a/bot/app/__main__.py
+++ b/bot/app/__main__.py
@@ -7,10 +7,10 @@
from message_handlers.setup import setup_message_handler
from web_apps.setup import setup_web_app
from forms.setup import setup_message_form
-from callback_data.main import Cb
+from callback_data.main import CbData
from aiohttp import web
from utils.db import DB
-import time
+from utils import subscriptions
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
@@ -24,29 +24,34 @@
db_host = os.environ['db_host']
db_port = os.environ['db_port']
- run_mode = os.environ.get('run_mode', 'standalone')
+ grafana_url = os.environ.get('grafana_url', 'http://127.0.0.1:3000/d/fDrj0_EGz/p2p-org-polkadot-kusama-dashboard?orgId=1')
+ prometheus_rules_url = os.environ.get('prometheus_rules_url', 'http://localhost:9090/api/v1/rules')
+ prometheus_alert_groups = os.environ.get('prometheus_alert_groups', [])
+ if isinstance(prometheus_alert_groups, str):
+ prometheus_alert_groups = prometheus_alert_groups.split(',')
+
web_app = web.Application()
db = DB(db_name,db_user,db_pass,db_host,db_port)
-
+ subs = subscriptions.Subscriptions(db, prometheus_rules_url, prometheus_alert_groups)
bot = Bot(token=tg_token, parse_mode="HTML")
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
-
+
router = Router()
dp.include_router(router)
- cb = Cb
+ cb = CbData
setup_message_handler('start')
-# setup_message_handler('support')
-# setup_message_form('support')
+ setup_message_form('sub_filter')
+ setup_message_form('support')
setup_web_app('ping')
setup_web_app('prom_alert')
- from callback_query_handlers import promalert,main_menu,grafana,support
+ from callback_query_handlers import promalert,main_menu,support,subscriptions
web_runner = web.AppRunner(web_app)
loop = asyncio.get_event_loop()
diff --git a/bot/app/callback_data/main.py b/bot/app/callback_data/main.py
index 3a51ebc..e7532f5 100644
--- a/bot/app/callback_data/main.py
+++ b/bot/app/callback_data/main.py
@@ -1,5 +1,5 @@
from aiogram.filters.callback_data import CallbackData
-class Cb(CallbackData, prefix="main"):
+class CbData(CallbackData, prefix="main"):
dst: str
data: str
diff --git a/bot/app/callback_query_handlers/grafana.py b/bot/app/callback_query_handlers/grafana.py
deleted file mode 100644
index 4874628..0000000
--- a/bot/app/callback_query_handlers/grafana.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from __main__ import router,db,bot,cb,admin_chat
-from aiogram.types import CallbackQuery,InlineKeyboardButton,InlineKeyboardMarkup
-from aiogram import F
-from utils.menu_builder import MenuBuilder
-from utils.functions import deploy,destroy
-from datetime import datetime, timezone
-
-@router.callback_query(cb.filter(F.dst == 'grafana'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- await query.answer(query.id)
-
- chat_id = query.message.chat.id
- message_id = query.message.message_id
-
- menu_builder = MenuBuilder()
-
- grafana_status = db.get_records('grafana_status','id',chat_id)
-
- if grafana_status == 'on':
- keyboard = menu_builder.build(callback_data=cb,preset='grafana_off',button_back='main_menu')
- await bot.send_message(chat_id,"According to our database you already have an instance 🤷\n\n",reply_markup=keyboard.as_markup())
- else:
- keyboard = menu_builder.build(callback_data=cb,preset='grafana_on',button_back='main_menu')
- await bot.send_message(chat_id,"Here you can setup your own grafana instance.\n\n",reply_markup=keyboard.as_markup())
-
- await bot.delete_message(chat_id,message_id)
-
-@router.callback_query(cb.filter(F.dst == 'grafana_on'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- await query.answer(query.id)
-
- username = query.message.chat.username
- chat_id = query.message.chat.id
- message_id = query.message.message_id
-
- deploy(chat_id,'./values.yml')
-
- db.update_record(chat_id,'grafana_status','on')
- db.update_record(chat_id,'grafana_deploy_time',datetime.now(timezone.utc))
-
-
- menu_builder = MenuBuilder()
- keyboard = menu_builder.build(callback_data=cb,button_back='grafana',button_main_menu=True)
-
- await bot.send_message(admin_chat, "Someone initialized of deploy grafana.\nUsername: @{username} ID: {chat_id}".format(username=username,chat_id=chat_id))
- await bot.send_message(chat_id,"Alright 👍\n\nOur robots started cooking your personal dashboard. Usually, this process takes around 5 minutes.\nWe will send all necessary data as soon as dashboard will be ready!\n\n",reply_markup=keyboard.as_markup())
-
- await bot.delete_message(chat_id,message_id)
-
-@router.callback_query(cb.filter(F.dst == 'grafana_off'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- await query.answer(query.id)
-
- username = query.message.chat.username
- chat_id = query.message.chat.id
- message_id = query.message.message_id
-
- destroy(chat_id,'./values.yml')
-
- db.update_record(chat_id,'grafana_status','off')
- #db.update_record(chat_id,'grafana_deploy_time',datetime.now(timezone.utc))
-
- menu_builder = MenuBuilder()
- keyboard = menu_builder.build(callback_data=cb,button_back='grafana',button_main_menu=True)
-
- await bot.send_message(admin_chat, "Someone initialized of destroy grafana.\nUsername: @{username} ID: {chat_id}".format(username=username,chat_id=chat_id))
- await bot.send_message(chat_id,"Alright 👍\n\nWe revoked your grafana instance",reply_markup=keyboard.as_markup())
-
- await bot.delete_message(chat_id,message_id)
diff --git a/bot/app/callback_query_handlers/main_menu.py b/bot/app/callback_query_handlers/main_menu.py
index 9135c09..2b30f26 100644
--- a/bot/app/callback_query_handlers/main_menu.py
+++ b/bot/app/callback_query_handlers/main_menu.py
@@ -1,17 +1,17 @@
-from __main__ import router,dp,db,bot,cb
-from aiogram.types import CallbackQuery,InlineKeyboardButton,InlineKeyboardMarkup
+from callback_data.main import CbData
+from __main__ import router
+from aiogram.types import CallbackQuery
from utils.menu_builder import MenuBuilder
from aiogram import F
-@router.callback_query(cb.filter(F.dst == 'main_menu'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- chat_id = query.message.chat.id
- message_id = query.message.message_id
+def main_menu():
+ return 'Please select', MenuBuilder().build(preset='main_menu')
- menu = MenuBuilder()
- menu = menu.build(callback_data=cb,preset='main_menu')
-
- await bot.send_message(chat_id,"Here is a main menu.",reply_markup=menu.as_markup())
- await bot.delete_message(chat_id,message_id)
- await query.answer(query.id)
+@router.callback_query(CbData.filter(F.dst == 'main_menu'))
+async def menu_prom_cb_handler(query: CallbackQuery):
+ text, keyboard = main_menu()
+
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('main menu')
diff --git a/bot/app/callback_query_handlers/promalert.py b/bot/app/callback_query_handlers/promalert.py
index 6a93f91..23ad61f 100644
--- a/bot/app/callback_query_handlers/promalert.py
+++ b/bot/app/callback_query_handlers/promalert.py
@@ -1,19 +1,55 @@
-from __main__ import router,dp,db,bot,cb
-from aiogram.types import CallbackQuery,InlineKeyboardButton,InlineKeyboardMarkup
+from callback_data.main import CbData
+from __main__ import router, db, subs
+from aiogram.types import CallbackQuery
+from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram import F
from utils.menu_builder import MenuBuilder
+from utils.db import DB
+from utils.subscriptions import Subscriptions
+from aiogram.fsm.context import FSMContext
+assert isinstance(db, DB)
+assert isinstance(subs, Subscriptions)
-@router.callback_query(cb.filter(F.dst == 'promalert'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- await query.answer(query.id)
-
- username = query.message.chat.username
- chat_id = query.message.chat.id
- message_id = query.message.message_id
+def promalert_dialog(promalert_status: str = '', chat_id: int = 0) -> (str, InlineKeyboardBuilder):
+ s = subs.get_subscriptions(chat_id)
+ if promalert_status == '':
+ promalert_status = db.get_records('promalert_status', 'id', chat_id)
+ text = []
+ if promalert_status != 'on':
+ promalert_status = 'off'
+ text.append(f'Notifications: {promalert_status}')
+ text.append(f'Subscriptions count: {len(s)}')
menu = MenuBuilder()
- menu = menu.build(callback_data=cb,preset='promalert',button_back='main_menu')
+ if promalert_status == 'off':
+ menu.add(preset='promalert_on')
+ else:
+ menu.add(preset='promalert_off')
+ menu.add(preset='sub_rules')
+ if len(s) > 0:
+ menu.add(preset='sub_list')
+ return '\n'.join(text), menu.build(button_back='main_menu')
- await bot.send_message(chat_id,"You can subscribe or deactivate subsription hete.",reply_markup=menu.as_markup())
- await bot.delete_message(chat_id,message_id)
+@router.callback_query(CbData.filter(F.dst == 'promalert'))
+async def handle_promalert(query: CallbackQuery):
+ text, keyboard = promalert_dialog(chat_id=query.message.chat.id)
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('alerts menu')
+
+@router.callback_query(CbData.filter(F.dst == 'promalert_on'))
+async def handle_promalert_on(query: CallbackQuery):
+ db.update_record(query.message.chat.id, 'promalert_status', 'on')
+ text, keyboard = promalert_dialog(chat_id=query.message.chat.id, promalert_status='on')
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('notifications enabled')
+
+@router.callback_query(CbData.filter(F.dst == 'promalert_off'))
+async def handle_promalert_off(query: CallbackQuery):
+ db.update_record(query.message.chat.id, 'promalert_status', 'off')
+ text, keyboard = promalert_dialog(chat_id=query.message.chat.id, promalert_status='off')
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('notifications disabled')
diff --git a/bot/app/callback_query_handlers/subscriptions.py b/bot/app/callback_query_handlers/subscriptions.py
new file mode 100644
index 0000000..c071db6
--- /dev/null
+++ b/bot/app/callback_query_handlers/subscriptions.py
@@ -0,0 +1,112 @@
+from aiogram.types import CallbackQuery
+from callback_data.main import CbData
+from __main__ import router, subs
+from utils.subscriptions import Subscriptions
+from utils.menu_builder import MenuBuilder
+from utils.msg_text import dict2text, text2dict
+from aiogram.fsm.context import FSMContext
+from aiogram import F
+from forms.sub_filter import Form, sub_filter_input_validate
+from callback_query_handlers.promalert import promalert_dialog
+assert isinstance(subs, Subscriptions)
+
+# Show rules list after Add subscription click
+@router.callback_query(CbData.filter(F.dst == 'sub_rules'))
+async def handle_sub_rules(query: CallbackQuery, callback_data: CbData):
+ r = await subs.get_rules()
+ rules = r.list()
+ keyboard = MenuBuilder().add(preset='rules_list', data=rules, navigation=callback_data.data)
+ call = query.message.edit_text(text='Please select alert you would like to subscribe to.',
+ reply_markup=keyboard.build().as_markup())
+ await query.bot(call)
+ await query.answer('Ok')
+
+# Show single rule for subscription and edit buttons
+@router.callback_query(CbData.filter(F.dst == 'sub_rule'))
+async def handle_sub_rule(query: CallbackQuery, callback_data: CbData):
+ r = await subs.get_rule_by_name(callback_data.data)
+ rule = r.dict()
+ keyboard = MenuBuilder()
+ keyboard.add(preset='sub_filter_edit', data=rule)
+ keyboard.add(preset='sub_save', data=r.alertname)
+ text = 'Use buttons to configure subscription "Edit ..." to specify filters, Save to subscribe or Back to cancel\n'
+ call = query.message.edit_text(text=text+dict2text(rule), reply_markup=keyboard.build(button_back='promalert').as_markup())
+ await query.bot(call)
+ await query.answer('Ok')
+
+# Show existing subscriptions
+@router.callback_query(CbData.filter(F.dst == 'sub_list'))
+async def handle_sub_list(query: CallbackQuery, callback_data: CbData):
+ if callback_data.data == '':
+ new_page = 0
+ else:
+ new_page = int(callback_data.data.split('/')[0])
+ s, total = subs.get_subscription_by_index(query.from_user.id, new_page)
+
+ keyboard = MenuBuilder()
+ keyboard.add(preset='sub_filter_edit', data=s.keys())
+ keyboard.add(preset='sub_del', data=s['alertname'][0])
+ keyboard.add(preset='sub_scroll', navigation=f'{new_page}/{total}')
+ call = query.message.edit_text(text=dict2text(s), reply_markup=keyboard.build().as_markup())
+ await query.bot(call)
+ await query.answer('Ok')
+
+# Turn on form.sub_filter and wait for field value
+@router.callback_query(CbData.filter(F.dst == 'sub_edit'))
+async def handle_sub_edit(query: CallbackQuery, callback_data: CbData, state: FSMContext):
+ d = text2dict(query.message.text)
+ await state.set_data({'expect': callback_data.data, 'current': d, 'message_id': query.message.message_id})
+ await state.set_state(Form.sub_filter)
+ text = f'Please send list of values for field {callback_data.data}\n'
+ keyboard = MenuBuilder()
+ call = query.message.edit_text(text=text, reply_markup=keyboard.build(button_back='sub_edit_cancel').as_markup())
+ await query.bot(call)
+ await query.answer(f'Please send values for: {callback_data.data}')
+
+# Cancel form.sub_filter go promalert
+@router.callback_query(CbData.filter(F.dst == 'sub_edit_cancel'))
+async def handle_sub_edit(query: CallbackQuery, state: FSMContext):
+ await state.clear()
+ text, keyboard = promalert_dialog(chat_id=query.from_user.id)
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('Canceled')
+
+# Save alert from text message to database
+@router.callback_query(CbData.filter(F.dst == 'sub_save'))
+async def handle_sub_save(query: CallbackQuery, callback_data: CbData, state: FSMContext):
+ await state.clear()
+ if callback_data.data == '':
+ await query.answer('Something went wrong. Please try later.')
+ sub = text2dict(query.message.text)
+ for key, val in sub.items():
+ valid, reason = sub_filter_input_validate(expected=key, got=val)
+ if not valid:
+ keyboard = MenuBuilder()
+ keyboard.add(preset='sub_filter_edit', data=sub)
+ keyboard.add(preset='sub_save', data=sub['alertname'][0])
+ text = f'Unable to subscribe: {reason}. Please edit {key} field.\n'
+ await query.bot.send_message(text=text+dict2text(sub), chat_id=query.from_user.id,
+ reply_markup=keyboard.build(button_back='promalert').as_markup())
+ await query.answer(f'Please update {key}')
+ return
+ n = subs.user_subscribe(chat_id=query.from_user.id, subscription_name=callback_data.data, lvs=sub)
+ if n == 0:
+ await query.answer('Something went wrong. Please try later.')
+ text, keyboard = promalert_dialog(chat_id=query.from_user.id)
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('Saved')
+
+# Delete subscription
+@router.callback_query(CbData.filter(F.dst == 'sub_del'))
+async def handle_sub_del(query: CallbackQuery, callback_data: CbData, state: FSMContext):
+ await state.clear()
+ if callback_data.data == '':
+ await query.answer('Something went wrong. Please try later.')
+ subs.user_unsubscribe(chat_id=query.from_user.id, subscription_name=callback_data.data)
+ text, keyboard = promalert_dialog(chat_id=query.from_user.id)
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('Deleted')
+
diff --git a/bot/app/callback_query_handlers/support.py b/bot/app/callback_query_handlers/support.py
index 4646bd7..f456196 100644
--- a/bot/app/callback_query_handlers/support.py
+++ b/bot/app/callback_query_handlers/support.py
@@ -1,39 +1,112 @@
-from __main__ import router,db,bot,cb
+from __main__ import router, db, bot
+from callback_data.main import CbData
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State,StatesGroup
+from callback_query_handlers.main_menu import main_menu
from forms.support import Form
from aiogram import F
from utils.menu_builder import MenuBuilder
-@router.callback_query(cb.filter(F.dst == 'support'))
-async def menu_prom_cb_handler(query: CallbackQuery,callback_data: cb):
- await query.answer(query.id)
- username = query.message.chat.username
+### User actions
+@router.callback_query(CbData.filter(F.dst == 'support'))
+async def handle_support_menu(query: CallbackQuery):
chat_id = query.message.chat.id
message_id = query.message.message_id
+ keyboard = MenuBuilder()
+ # don't even show support button to banned users
+ if db.get_records('account_status','id',chat_id) == 'on':
+ keyboard.add(preset='support_request')
- menu = MenuBuilder()
- keyboard = menu.build(callback_data=cb,button_back='main_menu')
-
- await bot.send_message(chat_id,"Useful things like wiki or chat will be later.\n\n",reply_markup=keyboard.as_markup())
+ await bot.send_message(chat_id,'Useful things like wiki or chat will be later.\n\n',reply_markup=keyboard.build(button_back='main_menu').as_markup())
await bot.delete_message(chat_id,message_id)
+ await query.answer('help menu')
-@router.callback_query(cb.filter(F.dst == 'support_on'))
-async def menu_support_cb_handler(query: CallbackQuery, state: FSMContext):
- await query.answer(query.id)
-
- username = query.message.chat.username
+@router.callback_query(CbData.filter(F.dst == 'support_request'))
+async def handle_support_request(query: CallbackQuery, state: FSMContext):
chat_id = query.message.chat.id
- message_id = query.message.message_id
-
support_status = db.get_records('support_status','id',chat_id)
-
if support_status == 'on':
- await bot.send_message(chat_id,"Please wait until we answer")
- await state.reset_state()
+ await bot.send_message(chat_id,'Please wait until we answer')
+ await state.clear()
+ await query.answer('Ok')
return
-
+
+ keyboard = MenuBuilder()
await state.set_state(Form.support)
- await bot.send_message(chat_id, "Please enter your message.")
+ call = query.message.answer(text=
+ 'Enter your question or describe the issue in one message.\n'+
+ 'Press Back if you changed your mind', reply_markup=keyboard.build(button_back='support_cancel').as_markup())
+
+ await query.bot(call)
+ db.update_record(chat_id,'support_status','on')
+ await query.answer('Ok')
+
+@router.callback_query(CbData.filter(F.dst == 'support_cancel'))
+async def handle_support_cancel(query: CallbackQuery, state: FSMContext):
+ await state.clear()
+ text, keyboard = main_menu()
+ call = query.message.edit_text(text=text, reply_markup=keyboard.as_markup())
+ await query.bot(call)
+ await query.answer('Got it. Hope you are okay')
+
+### Admin actions
+@router.callback_query(CbData.filter(F.dst == 'support_reply_cancel'))
+async def handle_support_reply_cancel(query: CallbackQuery, callback_data: CbData, state: FSMContext):
+ #db.update_record(int(callback_data.data), 'support_status', 'off')
+ await state.clear()
+ call = query.message.delete()
+ await query.answer('Response canceled')
+ await query.bot(call)
+
+@router.callback_query(CbData.filter(F.dst == 'support_reply_start'))
+async def handle_support_reply_start(query: CallbackQuery, callback_data: CbData, state: FSMContext):
+ try:
+ client_chat_id = int(callback_data.data)
+ except:
+ await query.answer('Something went wrong. chat_id not found.')
+ return
+
+ keyboard = MenuBuilder()
+ keyboard.add(preset='support_reply_cancel', data=str(client_chat_id))
+ call = query.message.reply(text=f'@{query.from_user.username} initiated conversation with {callback_data.data}. Please enter response text',
+ reply_markup=keyboard.build().as_markup())
+
+ await state.set_state(Form.admin_send_msg)
+ await state.set_data({'client_chat_id': client_chat_id})
+ await query.bot(call)
+ await query.answer('Ok')
+
+@router.callback_query(CbData.filter(F.dst == 'support_reply_submit'))
+async def handle_support_reply_submit(query: CallbackQuery, state: FSMContext):
+ d = await state.get_data()
+ try:
+ d['client_chat_id']
+ d['response']
+ except KeyError:
+ await query.answer('Outdated')
+ await query.bot.delete_message(chat_id=query.from_user.id, message_id=query.message.message_id)
+ return
+ await query.bot.send_message(chat_id=d['client_chat_id'], text=f'P2P support team:\n{d["response"]}')
+ db.update_record(d['client_chat_id'], 'support_status', 'off')
+ await state.clear()
+ await query.answer('Sent')
+
+@router.callback_query(CbData.filter(F.dst == 'support_off'))
+async def handle_support_off(query: CallbackQuery, callback_data: CbData):
+ db.update_record(int(callback_data.data), 'support_status', 'off')
+ await query.answer('Ok')
+
+@router.callback_query(CbData.filter(F.dst == 'toggle_ban'))
+async def handle_toggle_ban(query: CallbackQuery, callback_data: CbData):
+ chat_id = int(callback_data.data)
+ if db.get_records('account_status', 'id', chat_id) == 'off':
+ db.update_record(chat_id, 'account_status', 'on')
+ await bot.send_message(chat_id, 'Your account has been activated🤷\nHave a good day.')
+ await query.answer('Unbanned')
+ else:
+ db.update_record(chat_id, 'account_status', 'off')
+ db.update_record(chat_id, 'support_status', 'off')
+ await bot.send_message(chat_id, 'Your account has been disabled 🤷\nSorry and have a good day.')
+ await query.answer('Banned')
+
\ No newline at end of file
diff --git a/bot/app/forms/admin_support_reply.py b/bot/app/forms/admin_support_reply.py
deleted file mode 100644
index 7c17728..0000000
--- a/bot/app/forms/admin_support_reply.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __main__ import dp, db, bot
-from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
-from aiogram.dispatcher import FSMContext
-from aiogram.dispatcher.filters.state import State, StatesGroup
-from callback_query_handlers import admin_buttons
-
-class Form(StatesGroup):
- admin_send_msg = State()
-
-@dp.message_handler(state=Form.admin_send_msg)
-async def process_support(message: Message, state: FSMContext) -> None:
- username = message.chat.username
- chat_id = message.from_user.id
-
- if '/' in message.text:
- await message.answer("Unexpected charaster in your message.",reply_markup=ReplyKeyboardRemove())
- await state.reset_state()
- return
-
- menu = InlineKeyboardMarkup().add(InlineKeyboardButton(text="Continue", callback_data=admin_buttons.cb.new(action="support_open")),
- InlineKeyboardButton(text="Close", callback_data=admin_buttons.cb.new(action="support_close")))
-
-
- await bot.send_message(chat_id, "Message:\n{}".format(message.text),reply_markup=menu)
-
- await state.reset_state()
- db.update_record(chat_id,'support_status','off')
- return
diff --git a/bot/app/forms/sub_filter.py b/bot/app/forms/sub_filter.py
new file mode 100644
index 0000000..848e5d0
--- /dev/null
+++ b/bot/app/forms/sub_filter.py
@@ -0,0 +1,42 @@
+from __main__ import router, db
+from utils.db import DB
+from aiogram.types import Message
+from aiogram.fsm.context import FSMContext
+from aiogram.filters.state import State, StatesGroup
+from utils.menu_builder import MenuBuilder
+from utils.msg_text import dict2text
+
+assert isinstance(db, DB)
+
+class Form(StatesGroup):
+ sub_filter = State()
+
+@router.message(Form.sub_filter)
+async def handle_sub_filter_input(message: Message, state: FSMContext) -> None:
+ chat_id = message.from_user.id
+ d = await state.get_data()
+ await state.clear()
+ rule_name = d.get("rule")
+ if message.text and message.text != 'Back':
+ matchers = []
+ if rule_name != 'any':
+ matchers.append(('alertname', rule_name))
+ m = message.text.strip().replace(',', '\n')
+ d['current'][d['expect']] = []
+ for line in m.splitlines():
+ if line != '':
+ d['current'][d['expect']].append(line)
+ ######
+ keyboard = MenuBuilder()
+ keyboard.add(preset='sub_filter_edit', data=d['current'])
+ keyboard.add(preset='sub_save', data=d['current']['alertname'][0])
+ text = f'{d["expect"]} updated. Review changes and press Save to subscribe or Back to cancel.\n'
+ await message.bot.send_message(text=text+dict2text(d['current']), chat_id=chat_id, reply_markup=keyboard.build(button_back='promalert').as_markup())
+ else:
+ pass
+
+def sub_filter_input_validate(expected: str, got: [str]) -> (bool, str):
+ if expected == 'account':
+ if got == ['any']:
+ return False, 'Filter "any" for "account" is to open it will cause too many notifications.'
+ return True, ''
\ No newline at end of file
diff --git a/bot/app/forms/support.py b/bot/app/forms/support.py
index ee50d15..64f62de 100644
--- a/bot/app/forms/support.py
+++ b/bot/app/forms/support.py
@@ -1,4 +1,46 @@
+from __main__ import router, db, admin_chat
+from utils.db import DB
+from aiogram.types import Message
+from aiogram.fsm.context import FSMContext
from aiogram.filters.state import State, StatesGroup
+from utils.menu_builder import MenuBuilder
+assert isinstance(db, DB)
class Form(StatesGroup):
support = State()
+ admin_send_msg = State()
+
+@router.message(Form.support)
+async def handle_client_input(message: Message, state: FSMContext) -> None:
+ await state.clear()
+ keyboard = MenuBuilder()
+ keyboard.add(preset='support_reply_start',data=str(message.from_user.id))
+ keyboard.add(preset='support_off',data=str(message.from_user.id))
+ keyboard.add(preset='toggle_ban',data=str(message.from_user.id))
+ username = message.chat.username
+ if message.text:
+ await message.bot.send_message(admin_chat, f'Username: @{username}\nMessage:\n{message.text}\n',
+ reply_markup=keyboard.build().as_markup())
+ elif message.photo:
+ await message.bot.send_photo(admin_chat,message.photo[0].file_id)
+ await message.bot.send_message(admin_chat, f'Username: @{username} + \nCaption:\n{message.caption}\n',
+ reply_markup=keyboard.build().as_markup())
+
+ await message.answer('Please wait for the answer')
+
+@router.message(Form.admin_send_msg)
+async def handle_admin_input(message: Message, state: FSMContext) -> None:
+ keyboard = MenuBuilder()
+ keyboard.add(preset='support_reply_submit', data=str(message.from_user.id))
+ keyboard.add(preset='support_reply_cancel', data=str(message.from_user.id))
+ chat_id = message.from_user.id
+ try:
+ d = await state.get_data()
+ except:
+ message.answer("Could not read client chat_id")
+ return
+
+ d['response'] = message.text
+ await state.set_data(d)
+ await message.bot.send_message(chat_id, f'Are you sure you want to send following message to {d["client_chat_id"]}?:\n{message.text}',
+ reply_markup=keyboard.build().as_markup())
diff --git a/bot/app/message_handlers/start.py b/bot/app/message_handlers/start.py
index 706c36a..32c081a 100644
--- a/bot/app/message_handlers/start.py
+++ b/bot/app/message_handlers/start.py
@@ -1,9 +1,9 @@
-from __main__ import router,dp,db,bot,admin_chat,cb
+from __main__ import router,dp,db,admin_chat
from utils.menu_builder import MenuBuilder
from aiogram.types import Message
from aiogram import F
-@router.message(F.content_type.in_({'text', 'sticker','photo'}))
+@router.message(F.text == '/start')
async def command_start(message: Message) -> None:
if str(message.chat.id).startswith('-'):
await message.answer("🧑🤝🧑 Group chats are not allowed.\nSorry and have a good day.")
@@ -14,14 +14,16 @@ async def command_start(message: Message) -> None:
account_status = db.get_records('account_status','id',chat_id)
menu = MenuBuilder()
- menu = menu.build(callback_data=cb,preset='main_menu')
+ menu = menu.build(preset='main_menu')
if account_status and account_status == 'off':
await message.answer(chat_id,"Your account has been disabled 🤷\nSorry and have a good day.")
return
if not account_status:
- await bot.send_message(admin_chat, "Username: @{} ID: {}\nHas just PRE-registered.".format(username,chat_id))
+ keyboard = MenuBuilder()
+ keyboard.add(preset='toggle_ban',data=str(message.from_user.id))
+ await message.bot.send_message(admin_chat, text="Username: @{} ID: {}\nHas just PRE-registered.".format(username,chat_id), reply_markup=keyboard.build().as_markup())
db.add_account(chat_id,username)
await message.answer("Hi there 👋\n\n\nWelcome to a validator monitoring bot by P2P.org\n\n\n\n",reply_markup=menu.as_markup())
diff --git a/bot/app/message_handlers/support.py b/bot/app/message_handlers/support.py
deleted file mode 100644
index cc1aaaf..0000000
--- a/bot/app/message_handlers/support.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __main__ import router,db,bot,cb,admin_chat
-from aiogram.types import Message
-from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State,StatesGroup
-from forms.support import Form
-from aiogram import F
-from utils.menu_builder import MenuBuilder
-
-@router.message(state=Form.support)
-async def process_support(message: Message) -> None:
- username = message.chat.username
- chat_id = message.from_user.id
-
- menu = MenuBuilder()
- user_keyboard = menu.build(callback_data=cb,preset='support_text',button_back='support',button_main_meny=True)
- admin_keyboard = menu.build(callback_data=cb,preset='support_reply')
-
- if message.text:
- await bot.send_message(admin_chat, "Username: @{}\nMessage:\n{}\n\n".format(username,message.text),reply_markup=admin_keyboard.as_markup())
- elif message.photo:
- await bot.send_photo(admin_chat,message.photo[0].file_id)
- await bot.send_message(admin_chat, "Username: @{} + \nCaption: {}\n\n".format(username,message.caption),reply_markup=admin_keyboard.as_markup())
-
- await message.answer("Got it!!!\nYou will get an answer from our team soon.\n\n",reply_markup=user_keyboard.as_markup())
-
- await state.reset_state()
-
- db.update_record(chat_id,'support_status','on')
diff --git a/bot/app/utils/db.py b/bot/app/utils/db.py
index 9124461..24f943f 100644
--- a/bot/app/utils/db.py
+++ b/bot/app/utils/db.py
@@ -6,7 +6,17 @@
from psycopg import Error
from psycopg.sql import Identifier, Literal, SQL
+
class DB():
+
+ table_bot = Identifier('maas_bot_v1')
+ table_subscription = Identifier('maas_subscription')
+ col_id = Identifier('id')
+ col_chat_id = Identifier('chat_id')
+ col_subscription_name = Identifier('subscription_name')
+ col_key = Identifier('key_name')
+ col_value = Identifier('key_value')
+
def __init__(self, db_name, db_user, db_pass, db_host, db_port):
self.db_user = db_user
self.db_pass = db_pass
@@ -17,7 +27,7 @@ def __init__(self, db_name, db_user, db_pass, db_host, db_port):
self._cursor = None
self.init()
- def connect(self,retry_counter=0,sleep=5,attempts=5):
+ def connect(self, retry_counter=0, sleep=5, attempts=5):
if not self._connection:
try:
self._connection = psycopg.connect(dbname=self.db_name,
@@ -36,7 +46,7 @@ def connect(self,retry_counter=0,sleep=5,attempts=5):
retry_counter += 1
logging.warning('Could not conect to DB retry ' + str(retry_counter))
time.sleep(sleep)
- self.connect(retry_counter,sleep,attempts)
+ self.connect(retry_counter, sleep, attempts)
except (Exception, psycopg.Error) as error:
logging.error(error)
traceback.print_exc()
@@ -50,14 +60,14 @@ def cursor(self):
return self._cursor
- def query(self, query):
+ def query(self, query, compact=True):
result = []
-
+
if self._cursor:
if self._cursor.closed:
self.reset()
logging.error('Unable connect to DB')
-
+
try:
self._cursor.execute(query)
except (psycopg.errors.UniqueViolation, psycopg.errors.UndefinedColumn, psycopg.errors.InFailedSqlTransaction) as e:
@@ -70,7 +80,7 @@ def query(self, query):
colnames = [desc[0] for desc in self._cursor.description]
rows = self._cursor.fetchall()
- if len(list(rows)) == 1:
+ if len(list(rows)) == 1 and compact:
return rows[0][0]
for row in rows:
@@ -83,7 +93,7 @@ def query(self, query):
else:
self.reset()
logging.error('Unable connect to DB')
-
+
return result
def commit(self):
@@ -99,7 +109,7 @@ def init(self):
def reset(self):
self.close()
- if self.connect(0,1,2):
+ if self.connect(0, 1, 2):
self.cursor()
def close(self):
@@ -110,24 +120,96 @@ def close(self):
self._connection = None
self._cursor = None
- def get_records(self,field,key,value):
+ def get_records(self, field, key, value):
try:
- result = self.query(SQL('SELECT {} FROM {} WHERE {} = {}').format(Identifier(field),Identifier('maas_bot'),Identifier(key),Literal(value)))
- except (IndexError,KeyError):
+ result = self.query(SQL('SELECT {} FROM {} WHERE {} = {}').format(
+ Identifier(field),
+ DB.table_bot,
+ Identifier(key),
+ Literal(value)))
+ except (IndexError, KeyError):
result = None
return result
- def add_account(self,chat_id,username):
- values = SQL(', ').join(Literal(i) for i in [chat_id,username])
- self.query(SQL('INSERT INTO {} VALUES ({})').format(Identifier('maas_bot'),values))
+ def add_account(self, chat_id, username):
+ self.query(SQL('INSERT INTO {} VALUES ({})').format(
+ DB.table_bot,
+ SQL(', ').join([Literal(chat_id), Literal(username)])))
+
+ self.commit()
+ def update_record(self, chat_id, field, value):
+ self.query(SQL('UPDATE {} set {} = {} WHERE {} = {}').format(
+ DB.table_bot,
+ Identifier(field),
+ Literal(value),
+ DB.col_id,
+ Literal(chat_id)))
self.commit()
- def update_record(self,chat_id,field,value):
- self.query(SQL('UPDATE {} set {} = {} WHERE {} = {}').format(Identifier('maas_bot'),Identifier(field),Literal(value),Identifier('id'),Literal(chat_id)))
+ def delete_account(self, chat_id):
+ self.query(SQL('DELETE FROM {} WHERE {} = {}').format(
+ DB.table_bot,
+ DB.col_id,
+ Literal(chat_id)))
self.commit()
- def delete_account(self,chat_id):
- self.query(SQL('DELETE FROM {} WHERE {} = {}').format(Identifier('maas_bot'),Identifier('id'),Literal(chat_id)))
+ def get_subscriptions(self, chat_id):
+ try:
+ result = self.query(SQL('SELECT {subscription_name}, {label}, {match} FROM {table} WHERE {chat_id} = {val1}').format(
+ subscription_name=DB.col_subscription_name,
+ label=DB.col_key,
+ match=DB.col_value,
+ table=DB.table_subscription,
+ chat_id=DB.col_chat_id,
+ val1=Literal(chat_id)), compact=False)
+ except Exception as e:
+ result = None
+ return result
+
+ def add_or_update_subscription(self, chat_id, subscription_name, matchers):
+ if not isinstance(matchers, dict):
+ return 0
+
+ columns = [DB.col_chat_id, DB.col_subscription_name, DB.col_key, DB.col_value]
+ values = []
+
+ for key, vals in matchers.items():
+ for val in vals:
+ try:
+ values.append(SQL('({chat_id}, {subscription_name}, {key}, {value})').format(
+ chat_id=Literal(chat_id),
+ subscription_name=Literal(subscription_name),
+ key=Literal(key),
+ value=Literal(val)))
+ except IndexError:
+ return 0
+
+ try:
+ self.query(SQL('DELETE FROM {table} WHERE {chat_id} = {val1} AND {subscription_name} = {val2}').format(
+ table=DB.table_subscription,
+ chat_id=DB.col_chat_id,
+ val1=Literal(chat_id),
+ subscription_name=DB.col_subscription_name,
+ val2=Literal(subscription_name)))
+ self.query(SQL('INSERT INTO {table} ({columns}) VALUES {values}').format(
+ table=DB.table_subscription,
+ columns=SQL(',').join(columns),
+ values=SQL(',').join(values)))
+ self._connection.commit()
+ return len(values)
+ except Exception as e:
+ logging.error("unable to add/update subscription: {}".format(str(e)))
+ self._connection.rollback()
+ return 0
+
+ def delete_subscription(self, chat_id, subscription_name):
+ self.query(SQL('DELETE FROM {table} WHERE {chat_id} = {val1} AND {subscription_name} = {val2}').format(
+ table=DB.table_subscription,
+ chat_id=DB.col_chat_id,
+ val1=Literal(chat_id),
+ subscription_name=DB.col_subscription_name,
+ val2=Literal(subscription_name)
+ ))
self.commit()
diff --git a/bot/app/utils/functions.py b/bot/app/utils/functions.py
deleted file mode 100644
index 572b6d2..0000000
--- a/bot/app/utils/functions.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import yaml
-
-def deploy(chat_id,values_file):
- with open(values_file, 'r') as f:
- maas_apps = yaml.safe_load(f)
-
- if not next((app for app in maas_apps["applications"] if int(app) == int(chat_id)), None):
- maas_apps["applications"].append(chat_id)
- else:
- raise Exception("Aborting attempt to copy existing app. Chat ID: {}".format(chat_id))
-
- with open(values_file, "w") as f:
- yaml.dump(maas_apps, f)
-
-def destroy(chat_id,values_file):
- with open(values_file, 'r') as f:
- maas_apps = yaml.safe_load(f)
-
- for row in maas_apps["applications"]:
- if int(row) == int(chat_id):
- maas_apps['applications'].remove(row)
-
- with open(values_file, "w") as f:
- yaml.dump(maas_apps, f)
diff --git a/bot/app/utils/menu_builder.py b/bot/app/utils/menu_builder.py
index 7d27074..6c38f58 100644
--- a/bot/app/utils/menu_builder.py
+++ b/bot/app/utils/menu_builder.py
@@ -1,56 +1,134 @@
-from __main__ import bot,db
-from aiogram.utils.keyboard import InlineKeyboardBuilder
+from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
+from callback_data.main import CbData
+from __main__ import grafana_url
class MenuBuilder():
- def _preset_main_menu(self,callback_data):
- self.menu.button(text="Operate over Grafana instances", callback_data=callback_data(dst="grafana",data="").pack())
- self.menu.button(text="Operate over Prometheus alerts", callback_data=callback_data(dst="promalert",data="").pack())
- self.menu.button(text="Contact us(support)", callback_data=callback_data(dst='support',data="").pack())
+ def __init__(self) -> None:
+ self.menu = InlineKeyboardBuilder()
+
+ def _preset_main_menu(self):
+ self.menu.button(text="Open Grafana", url=grafana_url)
+ self.menu.button(text="Manage Prometheus alerts", callback_data=CbData(dst="promalert",data="").pack())
+ self.menu.button(text="Contact us(support)", callback_data=CbData(dst='support',data="").pack())
self.menu.adjust(1,1,1)
- def _preset_promalert(self,callback_data):
+ def _preset_promalert_on(self):
+ self.menu.button(text="Enable notifications", callback_data=CbData(dst="promalert_on",data="").pack())
+ self.menu.adjust(1)
- return self.menu
+ def _preset_promalert_off(self):
+ self.menu.button(text="Disable notifications", callback_data=CbData(dst="promalert_off",data="").pack())
+ self.menu.adjust(1)
- def _preset_grafana_on(self,callback_data):
- self.menu.button(text="Setup grafana", callback_data=callback_data(dst="grafana_on",data="").pack())
- self.menu.adjust(1)
+ def _preset_sub_rules(self):
+ self.menu.button(text="Add subscription", callback_data=CbData(dst="sub_rules",data="0").pack())
+ self.menu.adjust(1)
+
+ def _preset_sub_list(self):
+ self.menu.button(text="My subscriptions", callback_data=CbData(dst="sub_list",data="").pack())
+ self.menu.adjust(1)
- def _preset_grafana_off(self,callback_data):
- self.menu.button(text="Delete grafana", callback_data=callback_data(dst="grafana_off",data="").pack())
- self.menu.adjust(1)
+ def _preset_sub_save(self, data: str = ''):
+ self.menu.button(text="Save", callback_data=CbData(dst="sub_save",data=data).pack())
+ self.menu.adjust(1)
- def _preset_subscribtions(self,callback_data):
+ def _preset_sub_del(self, data: str = ''):
+ self.menu.button(text="Delete", callback_data=CbData(dst="sub_del",data=data).pack())
+ self.menu.adjust(1)
- return self.menu
+ def _preset_sub_filter_edit(self, data: [str] = []):
+ for k in data:
+ if k != 'alertname':
+ self.menu.row(InlineKeyboardButton(text=f"Edit {k}", callback_data=CbData(dst="sub_edit", data=k).pack(), width=1))
+
+ def _preset_rules_list(self, data: [str] = [], navigation: str = '0'):
+ skip = int(navigation)
+ take = 7
+
+ take = min(len(data) - skip, take)
+ for i in range(skip, skip+take):
+ self.menu.row(InlineKeyboardButton(text=data[i].alertname, callback_data=CbData(dst="sub_rule", data=data[i].alertname).pack()), width=1)
+ if len(data) > take:
+ prev_page = skip-take if skip-take >= 0 else len(data)-take
+ next_page = skip+take if skip+take < len(data) else 0
+ self.menu.row(*(
+ InlineKeyboardButton(text="<<", callback_data=CbData(dst="sub_rules", data=str(prev_page)).pack()),
+ InlineKeyboardButton(text=">>", callback_data=CbData(dst="sub_rules", data=str(next_page)).pack()),
+ InlineKeyboardButton(text="Back", callback_data=CbData(dst="promalert", data="").pack())
+ ))
+ else:
+ self.menu.row(InlineKeyboardButton(text="Back", callback_data=CbData(dst="promalert", data="").pack()))
+
+ def _preset_sub_scroll(self, navigation: str = '0/0'):
+ current_page, total_pages = [int(p) for p in navigation.split("/")]
+ if total_pages > 1:
+ next_page = current_page + 1 if current_page + 1 < total_pages else 0
+ prev_page = current_page - 1 if current_page - 1 >= 0 else total_pages - 1
+ self.menu.row(*(
+ InlineKeyboardButton(text="<<", callback_data=CbData(dst="sub_list", data=f"{prev_page}/{total_pages}").pack()),
+ InlineKeyboardButton(text=">>", callback_data=CbData(dst="sub_list", data=f"{next_page}/{total_pages}").pack()),
+ InlineKeyboardButton(text="Back", callback_data=CbData(dst="promalert", data="").pack())
+ ))
+ else:
+ self.menu.row(
+ InlineKeyboardButton(text="Back", callback_data=CbData(dst="promalert", data="").pack())
+ )
+
+ def _preset_support_request(self):
+ self.menu.button(text="Ask for help", callback_data=CbData(dst='support_request',data="").pack())
+ self.menu.adjust(1)
- def _preset_support_on(self,callback_data):
- self.menu.button(text="Contact us", callback_data=callback_data(dst='support_on',data="").pack())
+ def _preset_support_reply_start(self, data=""):
+ self.menu.button(text="Start reply", callback_data=CbData(dst='support_reply_start',data=data).pack())
self.menu.adjust(1)
- def _support_reply(self,callback_data):
- self.menu.button(text="Reply to client", callback_data=callback_data(dst='support_reply',data="").pack())
+ def _preset_support_reply_cancel(self, data=""):
+ self.menu.button(text="Cancel", callback_data=CbData(dst='support_reply_cancel',data=data).pack())
self.menu.adjust(1)
- def _button_main_menu(self,callback_data):
- self.menu.button(text="Main menu", callback_data=callback_data(dst='main_menu',data="").pack())
+ def _preset_support_reply_submit(self, data=""):
+ self.menu.button(text="Submit reply", callback_data=CbData(dst='support_reply_submit',data=data).pack())
self.menu.adjust(1)
- def _button_back(self,callback_data,dst):
- self.menu.button(text="Back", callback_data=callback_data(dst=dst,data="").pack())
+ def _preset_support_off(self, data=""):
+ self.menu.button(text="Support off", callback_data=CbData(dst='support_off',data=data).pack())
self.menu.adjust(1)
- def build(self,callback_data=None,preset: str = None, button_back: str = None, button_main_menu: bool = False):
- self.menu = InlineKeyboardBuilder()
+ def _preset_toggle_ban(self, data=""):
+ self.menu.button(text="Ban/Unban", callback_data=CbData(dst='toggle_ban',data=data).pack())
+ self.menu.adjust(1)
+
+ def _button_main_menu(self):
+ self.menu.button(text="Main menu", callback_data=CbData(dst='main_menu',data="").pack())
+ self.menu.adjust(1)
+ def _button_back(self,dst):
+ self.menu.button(text="Back", callback_data=CbData(dst=dst,data="").pack())
+ self.menu.adjust(1)
+
+ def build(self, preset: str = None, button_back: str = None, button_main_menu: bool = False):
if preset:
- eval('self._preset_' + preset)(callback_data)
+ eval('self._preset_' + preset)()
if button_main_menu == True:
- self._button_main_menu(callback_data)
+ self._button_main_menu()
if button_back:
- self._button_back(callback_data,button_back)
+ self._button_back(button_back)
return self.menu
+
+ def add(self, preset: str = None, data: any = None, navigation: str = ''):
+ if preset:
+ if data != None and navigation != '':
+ eval('self._preset_' + preset)(data=data, navigation=navigation)
+ return self
+ if data != None:
+ eval('self._preset_' + preset)(data=data)
+ return self
+ if navigation != '':
+ eval('self._preset_' + preset)(navigation=navigation)
+ return self
+ eval('self._preset_' + preset)()
+ return self
diff --git a/bot/app/utils/msg_text.py b/bot/app/utils/msg_text.py
new file mode 100644
index 0000000..b3f7413
--- /dev/null
+++ b/bot/app/utils/msg_text.py
@@ -0,0 +1,21 @@
+
+def text2dict(text: str):
+ if '---\n' in text:
+ text = text[text.index('---\n') + 4:]
+ d = dict()
+ key = ""
+ for l in text.strip('\n').splitlines():
+ if l == '':
+ continue
+ if l.endswith(":"):
+ key = l[:-1]
+ d[key] = []
+ else:
+ d[key].append(l)
+ return d
+
+def dict2text(d: dict):
+ t = '---'
+ for k in d.keys():
+ t = "{}\n{}:\n{}".format(t,k,'\n'.join(d[k]))
+ return t
\ No newline at end of file
diff --git a/bot/app/utils/subscriptions.py b/bot/app/utils/subscriptions.py
new file mode 100644
index 0000000..b19247e
--- /dev/null
+++ b/bot/app/utils/subscriptions.py
@@ -0,0 +1,143 @@
+
+from utils.db import DB
+import asyncio, aiohttp
+
+class Alert():
+ def __init__(self, prom_json: dict) -> None:
+ alert = prom_json.get('alerts', [{}])[0]
+
+ labels = alert.get('labels', {})
+ annotations = alert.get('annotations', {})
+
+ self.alertname = labels.get('alertname', '')
+ self.severity = labels.get('severity', '')
+ self.description = annotations.get('description', '')
+
+ self.kv = {'alertname': self.alertname}
+ for k, v in annotations.items():
+ if k not in ['description', 'summary']:
+ self.kv[k] = v
+
+class AlertRule:
+ def __init__(self, prom_rule: dict) -> None:
+ self.severity = prom_rule.get("labels", {}).get("severity", "info")
+ self.alertname = prom_rule.get("name")
+ annotations = prom_rule.get('annotations', {})
+ # save all possible annotation names exluding desc and summary
+ # eg: account, event_type, etc...
+ self.keys = [k for k in annotations if k not in ['description', 'summary']]
+
+
+ def __gt__(self, other):
+ return self.alertname > other.alertname
+
+ def dict(self):
+ d = dict()
+ d['alertname'] = [self.alertname]
+ for k in self.keys:
+ d[k] = ['any']
+ return d
+
+class AlertRules:
+ def __init__(self, prom_resp: dict, only_groups: [str] = []) -> None:
+ self.rules = dict()
+ self.only_groups = only_groups
+
+ # # successful resp sample
+ # status: "success"
+ # data:
+ # - groups:
+ # - name: ""
+ # rules:
+ # - name: ""
+ # annotations: {}
+ # labels: {}
+ if prom_resp.get("status", "") != "success":
+ raise Exception("unable to get 200 response from Prometheus API")
+ for group in prom_resp.get("data", {}).get("groups", []):
+ if self.only_groups == [] or group.get('name', '') in self.only_groups:
+ for rule in group.get("rules", []):
+ self.add(rule)
+
+ def add(self, prom_rule: dict) -> AlertRule:
+ r = AlertRule(prom_rule)
+ self.rules[r.alertname] = r
+ return r
+
+ def list(self) -> list[AlertRule]:
+ return sorted(self.rules.values())
+
+ def by_name(self, name) -> AlertRule:
+ try:
+ return self.rules[name]
+ except KeyError:
+ return None
+
+class Subscriptions:
+ def __init__(self, db: DB, rules_url: str, only_groups: list[str]=[]):
+ self.db = db
+ self.rules_url = rules_url
+ self.only_groups = only_groups
+
+ async def get_rule_by_name(self, name: str) -> AlertRule:
+ rules = await self.get_rules()
+ return rules.by_name(name)
+
+ async def get_rules(self) -> AlertRules:
+ e = None
+ for i in range(1, 10):
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(self.rules_url) as r:
+ resp = await r.json()
+ break
+ except Exception as err:
+ e = err
+ await asyncio.sleep(10)
+
+ if e != None:
+ raise(Exception("unable to read data from Prometheus API {0}, reason: {1}".format(self.rules_url, str(e))))
+
+ return AlertRules(prom_resp=resp, only_groups=self.only_groups)
+
+ def user_subscribe(self, chat_id: int, subscription_name: str, lvs: dict[str, any]):
+ return self.db.add_or_update_subscription(chat_id, subscription_name, lvs)
+
+ def must_notify(self, chat_id, a: Alert):
+ subs = self.get_subscriptions(chat_id)
+
+ for filters in subs.values():
+ matched = []
+ for key, values in filters.items():
+ for val in values:
+ if a.kv.get(key, '') == val or val == 'any':
+ matched.append(True)
+ if len(matched) == len(filters):
+ return True
+ return False
+
+ def user_unsubscribe(self, chat_id: int, subscription_name: str):
+ self.db.delete_subscription(chat_id, subscription_name)
+
+ def get_subscriptions(self, chat_id: int):
+ if chat_id == 0:
+ return {}
+ db_subs = self.db.get_subscriptions(chat_id)
+ subs = {}
+ for sub in db_subs:
+ subscription_name = sub.get('subscription_name')
+ if subs.get(subscription_name) == None:
+ subs[subscription_name] = {}
+ key = sub.get('key_name')
+ value = sub.get('key_value')
+ if subs[subscription_name].get(key) == None:
+ subs[subscription_name][key] = [value]
+ else:
+ subs[subscription_name][key].append(value)
+ return subs
+
+ def get_subscription_by_index(self, chat_id: int, index: int):
+ subs = self.get_subscriptions(chat_id=chat_id)
+ keys = sorted(subs.keys())
+ if index >= 0 and index < len(keys) and len(keys) >= 1:
+ return subs[keys[index]], len(keys)
diff --git a/bot/app/values.yml b/bot/app/values.yml
deleted file mode 100644
index 76901dc..0000000
--- a/bot/app/values.yml
+++ /dev/null
@@ -1 +0,0 @@
-applications: []
diff --git a/bot/app/web_apps/prom_alert.py b/bot/app/web_apps/prom_alert.py
index 0c112cd..503efc1 100644
--- a/bot/app/web_apps/prom_alert.py
+++ b/bot/app/web_apps/prom_alert.py
@@ -1,36 +1,27 @@
-from __main__ import db, bot, web, web_app
+from __main__ import db, bot, web, web_app, subs
from aiohttp.web_request import Request
from aiohttp.web_response import json_response
from aiogram.types import WebAppInfo
from aiogram.types import ReplyKeyboardRemove
+import logging
+from utils.subscriptions import Alert
async def handler(request: Request):
- alert = await request.json()
+ a = await request.json()
+ alert = Alert(a)
- try:
- severity = alert['alerts'][0]['labels']['severity']
- except (IndexError,KeyError):
- severity = None
-
- try:
- alertname = alert['alerts'][0]['labels']['alertname']
- except (IndexError,KeyError):
- alertname = None
-
- try:
- message = alert['alerts'][0]['annotations']['description']
- except (IndexError,KeyError):
- message = None
-
- if severity and alertname:
+ if alert.severity and alert.alertname:
ids = db.get_records('id','promalert_status','on')
if isinstance(ids, int):
ids = [{'id':ids}]
-
for i in ids:
- await bot.send_message(i['id'], "Prometheus alerting\n\nSeverity: {}\nAlert name: {}\nMessage: {}\n\nYou can always disable notification such this by calling /promalert\n\nFeel free to contact us /support if any questions.".format(severity,alertname,message),reply_markup=ReplyKeyboardRemove())
+ if subs.must_notify(i['id'], alert):
+ await bot.send_message(i['id'], '\n'.join([
+ f'Alert: {alert.alertname}',
+ f'Severity: {alert.severity}',
+ f'Description: {alert.description}']))
return web.json_response({'status':'ok'}) ## To be reviewed
diff --git a/bot/db_scheme.sql b/bot/db_scheme.sql
index 61f814d..d50eff7 100644
--- a/bot/db_scheme.sql
+++ b/bot/db_scheme.sql
@@ -23,19 +23,29 @@ SET default_table_access_method = heap;
--
-- Name: maas_bot; Type: TABLE; Schema: public; Owner: adm
--
-CREATE TABLE public.maas_bot (
+CREATE TABLE public.maas_bot_v1 (
id bigint NOT NULL,
username text,
account_status text DEFAULT 'on',
- grafana_status text DEFAULT 'off',
promalert_status text DEFAULT 'off',
support_status text DEFAULT 'off',
- creation_time timestamp without time zone DEFAULT now(),
- grafana_deploy_time timestamp without time zone
+ creation_time timestamp without time zone DEFAULT now()
);
-ALTER TABLE public.maas_bot OWNER TO adm;
-ALTER TABLE ONLY public.maas_bot
- ADD CONSTRAINT maas_bot_pkey PRIMARY KEY (id);
+ALTER TABLE public.maas_bot_v1 OWNER TO adm;
+ALTER TABLE ONLY public.maas_bot_v1
+ ADD CONSTRAINT maas_bot_v1_pkey PRIMARY KEY (id);
+
+CREATE TABLE public.maas_subscription (
+ id SERIAL PRIMARY KEY,
+ chat_id bigint NOT NULL,
+ subscription_name text NOT NULL,
+ key_name text NOT NULL,
+ key_value text NOT NULL,
+ CONSTRAINT fk_chat_id
+ FOREIGN KEY(chat_id)
+ REFERENCES public.maas_bot_v1(id)
+);
+ALTER TABLE public.maas_subscription OWNER TO adm;
--
-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: adm
--
diff --git a/docker-compose.yml b/docker-compose.yml
index 1291160..1a62f9c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,7 @@ services:
networks:
- exporters
- monitoring
+ - bot
grafana:
image: grafana/grafana:latest
@@ -52,15 +53,16 @@ services:
environment:
- "db_host=postgres"
- "db_port=5432"
+ - "prometheus_rules_url=http://prometheus:9090/api/v1/rules"
+ - "prometheus_alert_groups=maas-rules"
env_file:
- ./bot.env
- volumes:
- - ./bot/app/values.yml:/app/values.yml
ports:
- "8080:8080"
networks:
- bot
- postgres
+ - monitoring
postgres:
container_name: postgres
diff --git a/polkadot.yml b/polkadot.yml
index 89e8a1d..cce489b 100644
--- a/polkadot.yml
+++ b/polkadot.yml
@@ -15,7 +15,7 @@ services:
networks:
- exporters
restart: on-failure
-
+
polkadot_finality_exporter:
build:
context: ./exporters/common
@@ -30,7 +30,7 @@ services:
networks:
- exporters
restart: on-failure
-
+
polkadot_events_exporter:
build:
context: ./exporters/events
diff --git a/prometheus/alerts.yml b/prometheus/alerts.yml
index f93c09a..b821386 100644
--- a/prometheus/alerts.yml
+++ b/prometheus/alerts.yml
@@ -1,30 +1,37 @@
---
+## Please never rename alert names without corresponding migration in DB
groups:
-- name: maas rules
+- name: maas-rules
rules:
- - alert: Kusama GRANDPA precommits ratio(critical)
- expr: (polkadot_finality_precommits{chain="kusama"} / on (chain) group_left() polkadot_finality_roundsProcessed * 100) < 10 and on(instance) polkadot_session_validators == 1 and on (chain) polkadot_session_sessionProgress >= 18
+ - alert: Validator left set
+ expr: polkadot_session_disabledValidators == 1
+ for: 1m
+ labels:
+ severity: "critical"
+ annotations:
+ summary: "{{ $labels.account }}: Validator has been kicked from the active set."
+ description: "Validator {{ $labels.account }} has been kicked from the active set."
+ chain: "{{ $labels.chain }}"
+ account: "{{ $labels.account }}"
+
+ - alert: Low GRANDPA precommits ratio
+ expr: (polkadot_finality_precommits{chain="polkadot"} / on (chain) group_left() polkadot_finality_roundsProcessed * 100) < 10 and on(node) polkadot_session_validators == 1 and on (chain) polkadot_session_sessionProgress >= 3
for: 2m
labels:
severity: critical
annotations:
- summary: "{{ $labels.account }}: Validator has problem with block finality(precommits) less than 33%"
- description: "Validator {{ $labels.account }} has problem with block finality(precommits) less than 33%"
+ summary: "{{ $labels.account }}: Validator has problem with block finality(precommits) less than 10%"
+ description: "Validator {{ $labels.account }} has problem with block finality(precommits) less than 10%"
+ chain: "{{ $labels.chain }}"
+ account: "{{ $labels.account }}"
- - alert: Kusama GRANDPA prevotes ratio
- expr: (polkadot_finality_prevotes{chain="kusama"} / on (chain) group_left() polkadot_finality_roundsProcessed * 100) < 80 and on(instance) polkadot_session_validators == 1 and on (chain) polkadot_session_sessionProgress >= 18
+ - alert: Low GRANDPA prevotes ratio
+ expr: (polkadot_finality_prevotes{chain="polkadot"} / on (chain) group_left() polkadot_finality_roundsProcessed * 100) < 80 and on(node) polkadot_session_validators == 1 and on (chain) polkadot_session_sessionProgress >= 3
for: 2m
labels:
severity: high
annotations:
summary: "{{ $labels.account }}: Validator has problem with block finality(prevotes) less than 80%"
description: "Validator {{ $labels.account }} has problem with block finality(prevotes) less than 80%"
-
- - alert: Earned points(Total rate)
- expr: sum by (account)(rate(polkadot_staking_eraPoints[1m])) == 0
- for: 10m
- labels:
- severity: "critical"
- annotations:
- summary: "{{ $labels.account }}: not producing blocks for long time."
- description: "Validator {{ $labels.account }} has a problem with blocks producing."
\ No newline at end of file
+ chain: "{{ $labels.chain }}"
+ account: "{{ $labels.account }}"