diff --git a/CHANGELOG.md b/CHANGELOG.md
index a61cd88..b37721f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,16 @@
+## [0.0.8]
+
+- генерация древовидного графа, с опорой на одного участника
+- /my для просмотра и изменения связей
+- возможность отмены поручительства
+- рефакторинг
+
+
## [0.0.7]
-- исправления
-- команда, генерирующая граф связей
+- мьют на входе, там где заявки не включены
+- одобрение заявки: любой участник может поручиться
+- за всех кто уже в чате и пишет сообщения
## [0.0.6]
diff --git a/api/webhook.py b/api/webhook.py
index 15f2b84..7e5acd8 100644
--- a/api/webhook.py
+++ b/api/webhook.py
@@ -1,11 +1,19 @@
-from tgbot.config import WEBHOOK, FEEDBACK_CHAT_ID # init storage there
-from tgbot.handlers import handle_feedback, handle_answer, \
- handle_join, handle_left, handle_button, handle_join_request, \
- handle_graph
-from tgbot.api import register_webhook
from sanic import Sanic
from sanic.response import text
+from tgbot.config import WEBHOOK, FEEDBACK_CHAT_ID
+
+from tgbot.handlers.handle_feedback import handle_feedback, handle_answer
+from tgbot.handlers.handle_members_change import handle_join, handle_left
+from tgbot.handlers.handle_join_request import handle_join_request
+from tgbot.handlers.handle_default import handle_default
+from tgbot.handlers.command_my import handle_command_my
+from tgbot.handlers.command_graph import handle_command_graph
+from tgbot.handlers.callback_vouch import handle_button
+from tgbot.handlers.callback_unlink import handle_unlink
+
+from tgbot.api import register_webhook
+
app = Sanic()
app.config.REGISTERED = False
@@ -13,14 +21,15 @@ app.config.REGISTERED = False
@app.route('/', methods=["GET"])
async def register(req):
+ res = 'skipped'
if not app.config.REGISTERED:
r = register_webhook(WEBHOOK)
- print(f'\n\t\t\tWEBHOOK REGISTERED:\n{r.json()}')
+ print(f'\n\t\t\tWEBHOOK REGISTERED:\n{r}')
app.config.REGISTERED = True
- print(r.json())
- return text('ok')
- return text('skipped')
-
+ print(r)
+ res = 'ok'
+ return text(res)
+
@app.post('/')
async def handle(req):
@@ -28,25 +37,45 @@ async def handle(req):
try:
update = req.json
print(update)
- if 'message' in update:
- msg = update.get('message', update.get('edited_message'))
- if msg['chat']['type'] == 'private':
- handle_feedback(msg)
+
+ # видимые сообщения
+ msg = update.get('message', update.get('edited_message'))
+ if msg:
+ if msg['chat']['id'] == msg['from']['id']:
+ if msg['text'] == '/my':
+ handle_command_my(msg)
+ else:
+ handle_feedback(msg)
elif str(msg['chat']['id']) == FEEDBACK_CHAT_ID:
if 'reply_to_message' in msg:
handle_answer(msg)
elif 'text' in msg and msg['text'] == '/graph':
- handle_graph(msg)
+ await handle_command_graph(msg)
+ elif 'new_chat_member' in msg:
+ handle_join(msg)
+ elif 'left_chat_member' in msg:
+ handle_left(msg)
else:
- if 'new_chat_member' in msg:
- handle_join(msg)
- elif 'left_chat_member' in msg:
- handle_left(msg)
+ handle_default(msg)
+
+ # кнопки
elif 'callback_query' in update:
- handle_button(update['callback_query'])
+ data = update['callback_query']['data']
+ if data.startswith(BUTTON_VOUCH):
+ handle_button(update['callback_query'])
+ elif data.startswith('unlink'):
+ handle_unlink(update['callback_query'])
+
+ # заявки
elif 'chat_join_request' in update:
print('chat join request')
- handle_join_request(update)
+ handle_join_request(update['chat_join_request'])
+
+ # wtf
+ else:
+ print('UNHANDLED EVENT')
+
+
except Exception:
import traceback
traceback.print_exc()
diff --git a/requirements.txt b/requirements.txt
index db1d882..9d3ddd8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
sanic==19.6.0
requests
redis
-cairosvg
\ No newline at end of file
+aiohttp
\ No newline at end of file
diff --git a/tgbot/api.py b/tgbot/api.py
index 58aeb49..045757d 100644
--- a/tgbot/api.py
+++ b/tgbot/api.py
@@ -1,4 +1,5 @@
import requests
+import aiohttp
import json
import os
@@ -10,15 +11,16 @@ def register_webhook(url):
r = requests.get(
apiBase + f'setWebhook?url={url}'
)
- return r
+ return r.json()
def delete_message(cid: str, mid: str):
url = apiBase + f"deleteMessage?chat_id={cid}&message_id={mid}"
r = requests.post(url)
- return r
+ return r.json()
+# https://core.telegram.org/bots/api#sendmessage
def send_message(cid: str, body, reply_to=None, reply_markup=None):
url = apiBase + f"sendMessage?chat_id={cid}&text={body}"
if reply_to:
@@ -27,9 +29,10 @@ def send_message(cid: str, body, reply_to=None, reply_markup=None):
reply_markup = json.dumps(reply_markup)
reply_markup = requests.utils.quote(reply_markup)
url += f'&reply_markup={reply_markup}'
+ url += f'&parse_mode=HTML'
r = requests.post(url)
print(f'{url}')
- return r
+ return r.json()
# https://core.telegram.org/bots/api#banchatmember
@@ -38,27 +41,27 @@ def ban_member(chat_id, user_id, until_date=None):
if until_date:
url += f'&until_data={until_date}'
r = requests.post(url)
- return r
+ return r.json()
# https://core.telegram.org/bots/api#unbanchatmember
def unban_member(chat_id, user_id):
url = apiBase + f"unbanChatMember?chat_id={chat_id}&user_id={user_id}&only_if_banned=1"
r = requests.post(url)
- return r
+ return r.json()
# https://core.telegram.org/bots/api#addchatmember
def add_member(chat_id, user_id):
url = apiBase + f"addChatMember?chat_id={chat_id}&user_id={user_id}"
r = requests.post(url)
- return r
+ return r.json()
def forward_message(cid, mid, to_chat_id):
url = apiBase + f"forwardMessage?chat_id={to_chat_id}" + \
f"&from_chat_id={cid}&message_id={mid}"
r = requests.post(url)
- return r
+ return r.json()
# https://core.telegram.org/bots/api#restrictchatmember
@@ -66,9 +69,9 @@ def mute_member(chat_id, member_id):
chat_permissions = json.dumps({ "can_send_messages": False })
chat_permissions = requests.utils.quote(chat_permissions)
url = apiBase + f'restrictChatMember?chat_id={chat_id}' + \
- f'&user_id={user_id}&permissions={chat_permissions}'
+ f'&user_id={member_id}&permissions={chat_permissions}'
r = requests.post(url)
- return r
+ return r.json()
# https://core.telegram.org/bots/api#restrictchatmember
@@ -76,22 +79,69 @@ def unmute_member(chat_id, member_id):
chat_permissions = json.dumps({ "can_send_messages": True })
chat_permissions = requests.utils.quote(chat_permissions)
url = apiBase + f'restrictChatMember?chat_id={chat_id}' + \
- f'&user_id={user_id}&permissions={chat_permissions}'
+ f'&user_id={member_id}&permissions={chat_permissions}'
r = requests.post(url)
- return r
+ return r.json()
+
# https://core.telegram.org/bots/api#approvechatjoinrequest
def approve_chat_join_request(chat_id, user_id):
url = apiBase + f"approveChatJoinRequest?chat_id={chat_id}" + \
f'&user_id={user_id}'
r = requests.post(url)
- return r
+ return r.json()
-def send_graph(png_data, chat_id):
+# https://core.telegram.org/bots/api#senddocument
+async def send_document(chat_id, data='', filename='chart.svg'):
+ url = apiBase + "sendDocument"
+ params = { "chat_id": chat_id }
+ files = { "document": data }
+
+ async with aiohttp.ClientSession(headers=headers) as session:
+ async with session.post(url, params=params, data=filedata) as response:
+ if response.status != 200:
+ error_text = await response.text()
+ print(f"Error sending document: {response.status} - {error_text}")
+ return None
+
+ try:
+ return await response.json()
+ except ValueError as e:
+ print(f"Error decoding JSON: {e}")
+ return None
+
+
+
+# https://core.telegram.org/bots/api#sendphoto
+async def send_photo(png_data, chat_id):
url = apiBase + f"sendPhoto"
headers = {"Content-Type": "multipart/form-data"}
files = {"photo": ("chart.png", png_data)}
params = {"chat_id": chat_id}
- response = requests.post(url, headers=headers, files=files, params=params)
- return response.json()
\ No newline at end of file
+
+ async with aiohttp.ClientSession(headers=headers) as session:
+ async with session.post(url, params=params, data=filedata) as response:
+ if response.status != 200:
+ print(f"Error sending photo: {response.status}")
+ return None
+
+ try:
+ return await response.json()
+ except ValueError as e:
+ print(f"Error decoding JSON: {e}")
+ return None
+
+
+# https://core.telegram.org/bots/api#getchatadministrators
+def get_chat_administrators(chat_id):
+ url = apiBase + f"getChatAdministrators?chat_id={chat_id}"
+ r = requests.get(url)
+ return r.json()
+
+
+# # https://core.telegram.org/bots/api#getchatmember
+def get_member(chat_id, member_id):
+ url = apiBase + f"getChatMember?chat_id={chat_id}&user_id={member_id}"
+ r = requests.get(url)
+ return r.json()
\ No newline at end of file
diff --git a/tgbot/graph.py b/tgbot/graph.py
deleted file mode 100644
index bb165b4..0000000
--- a/tgbot/graph.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import cairosvg
-
-def generate_chart(members):
- # Размеры прямоугольника узла
- node_width = 150
- node_height = 50
-
- # Размеры холста
- canvas_width = 800
- canvas_height = 600
-
- # Радиус узла (для закругленных прямоугольников)
- node_radius = 10
-
- # Цвета
- background_color = "#F2F2F2"
- node_color = "#EFEFEF"
- node_stroke_color = "#999"
- node_text_color = "#333"
- line_color = "#CCC"
-
- # Список строк SVG-кода
- svg_lines = []
-
- # Рассчитываем координаты для каждого узла
- coordinates = {}
- for member in members:
- x = member['x']
- y = member['y']
- coordinates[member['id']] = {'x': x, 'y': y}
-
- # Рисуем линии-связи между узлами
- for member in members:
- member_id = member['id']
- x1 = coordinates[member_id]['x'] * node_width + node_width / 2
- y1 = coordinates[member_id]['y'] * node_height + node_height / 2
- for parent_id in member['parents']:
- x2 = coordinates[parent_id]['x'] * node_width + node_width / 2
- y2 = coordinates[parent_id]['y'] * node_height + node_height / 2
- svg_lines.append(f'')
-
- # Рисуем узлы
- for member in members:
- member_id = member['id']
- x = coordinates[member_id]['x'] * node_width
- y = coordinates[member_id]['y'] * node_height
-
- # Рисуем фоновый прямоугольник
- svg_lines.append(f'')
-
- # Добавляем текст в центр узла
- member_name = member['name'][:16]
- text_x = x + node_width / 2
- text_y = y + node_height / 2
- svg_lines.append(f'{member_name}')
-
- # Создаем SVG-код
- svg = f''
- # конвертировать SVG в PNG
- png_data = cairosvg.svg2png(bytestring=svg_data)
- return png_data
diff --git a/tgbot/handlers.py b/tgbot/handlers.py
deleted file mode 100644
index a39f518..0000000
--- a/tgbot/handlers.py
+++ /dev/null
@@ -1,185 +0,0 @@
-from tgbot.api import send_message, forward_message, delete_message, \
- ban_member, unban_member, mute_member, unmute_member, \
- approve_chat_join_request, send_graph
-from tgbot.graph import generate_chart
-from tgbot.config import REDIS_URL, FEEDBACK_CHAT_ID, BUTTON_VOUCH, NEWCOMER_MSG
-import json
-import redis
-from tgbot.profile import Profile as ProfileObj
-
-
-# сохраняет сессии и пересылаемые сообщения между перезагрузками
-storage = redis.from_url(REDIS_URL)
-
-# хранение необходимой информации о пользователях
-Profile = ProfileObj(storage)
-
-def newcomer_show(msg):
- reply_markup = {
- "inline_keyboard": [
- [
- {
- "text": BUTTON_VOUCH,
- "callback_data": BUTTON_VOUCH + str(msg['from']['id'])
- }
- ]
- ]
- }
- identity = f"{msg['from']['first_name']} {msg['from'].get('last_name', '')}"
- if 'username' in msg['from']:
- identity += f" @{msg['from']['username']}"
- r = send_message(
- msg['chat']['id'],
- NEWCOMER_MSG + identity,
- reply_to=msg['message_id'],
- reply_markup=reply_markup
- )
- welcome_msg_id = r.json()['result']['message_id']
- print(r.json())
- print(f'welcome message id: {welcome_msg_id}')
- return welcome_msg_id
-
-
-def handle_feedback(msg):
- mid = msg['message_id']
- cid = msg['chat']['id']
- r = forward_message(cid, mid, FEEDBACK_CHAT_ID).json()
- support_msg_id = r['result']['message_id']
- # сохранение айди сообщения в приватной переписке с ботом
- storage.set(f'fbk-{support_msg_id}', json.dumps({
- "author_id": msg["from"]["id"],
- "message_id": mid,
- "chat_id": cid
- }))
-
-
-def handle_answer(msg):
- print(f'handle answer from support')
- support_msg_id = str(msg['reply_to_message']['message_id'])
- # получение сохраненного айди сообщения из личной переписки с ботом
- stored_feedback = storage.get(f'fbk-{support_msg_id}')
- stored_feedback = json.loads(stored_feedback)
- r = send_message(f'{stored_feedback["chat_id"]}', msg['text'], reply_to=stored_feedback["message_id"]) # notice 'u' before private chat ID
- print(r.json())
-
-
-def handle_join(msg):
- chat_id = str(msg['chat']['id'])
- from_id = str(msg['from']['id'])
- actor = Profile.get(from_id)
-
- actor["name"] = msg['from']['first_name'] + msg['from'].get('last_name', '')
- actor["mention"] = msg['from'].get('username', '')
- newcomer_id = str(msg['new_chat_member']['id'])
- if from_id == newcomer_id:
- if len(actor['parents']) == 0:
- # показываем сообщение с кнопкой "поручиться"
- welcome_msg_id = newcomer_show(msg)
-
- # до одобрения - мьют
- r = mute_member(chat_id, newcomer_id)
- print(r.json())
-
- # обновляем профиль присоединившегося
- actor['welcome_id'] = welcome_msg_id
- Profile.save(actor)
- else:
- # за пользователя поручились ранее
- r = delete_message(chat_id, actor['welcome_id'])
- print(r.json())
- else:
- # пользователи приглашены другим участником
- print(f'{len(msg["new_chat_members"])} members were invited by {from_id}')
- for m in msg['new_chat_members']:
- newcomer = Profile.get(m['id'])
- newcomer['parents'].append(from_id)
- Profile.save(newcomer)
- actor['children'].append(m['id'])
- r = unmute_member(chat_id, newcomer['id'])
- print(r.json())
-
- # обновляем профиль пригласившего
- Profile.save(actor)
-
-
-def handle_left(msg):
- print(f'handling member leaving')
- member_id = msg["left_chat_member"]["id"]
-
- # профиль покидающего чат
- leaver = Profile.get(member_id)
-
- # удаление сообщения с кнопкой
- r = delete_message(msg['chat']['id'], leaver['welcome_id'])
- print(r.json())
-
- Profile.leaving(leaver)
-
-
-def handle_button(callback_query):
- # получаем профиль нажавшего кнопку
- actor_id = str(callback_query['from']['id'])
- actor = Profile.get(actor_id)
-
- callback_data = callback_query['data']
- if callback_data.startswith(BUTTON_VOUCH):
- print(f'vouch button pressed by {actor_id}')
-
- newcomer_id = callback_data[len(BUTTON_VOUCH):]
- newcomer = Profile.get(newcomer_id)
- if newcomer_id not in actor['children'] and \
- actor_id not in newcomer['parents']:
- newcomer['parents'].append(newcomer_id)
- actor['children'].append(actor_id)
- Profile.save(newcomer)
- Profile.save(actor)
- try:
- chat_id = str(callback_query['message']['chat']['id'])
-
- print('unmute newcomer')
- r = unmute_member(chat_id, newcomer_id)
- print(r.json())
-
- print('accept join request')
- r = approve_chat_join_request(chat_id, newcomer_id)
- print(r.json())
-
- except:
- pass
-
-
-def handle_join_request(update):
- print(f'handle join request')
- chat_id = str(update['message']['chat']['id'])
- from_id = str(update['message']['from']['id'])
- actor = Profile.get(from_id)
-
- actor["name"] = update['message']['from']['first_name'] + update['message']['from'].get('last_name', '')
- actor["mention"] = update['message']['from'].get('username', '')
-
- if from_id == str(update['message']['new_chat_member']['id']):
- if len(actor['parents']) == 0:
- # показываем сообщение с кнопкой "поручиться"
- welcome_msg_id = show_request_msg(update)
-
-
-def handle_graph(_msg):
- cursor = 0
- keys = []
- while True:
- # Scan for keys starting with 'urs-*' in batches of 100
- cursor, batch_keys = r.scan(cursor=cursor, match='urs-*', count=100)
- keys += batch_keys
- # If the cursor is 0, then we've reached the end of the keys
- if cursor == 0:
- break
- # Get the values of all the keys
- values = r.mget(keys)
- # Parse the JSON data from each value
- members = []
- for value in values:
- member = json.loads(value)
- members.append(member)
- png_data = generate_chart(values)
- r = send_graph(png_data, chat_id)
- print(r.json())
diff --git a/tgbot/handlers/callback_unlink.py b/tgbot/handlers/callback_unlink.py
new file mode 100644
index 0000000..cf87eb4
--- /dev/null
+++ b/tgbot/handlers/callback_unlink.py
@@ -0,0 +1,23 @@
+from tgbot.api import send_message, delete_message
+from tgbot.storage import Profile
+
+# remove link of callback sender
+# from member vouched before
+def handle_unlink(callback_query):
+ print('handle unlink button pressed, private chat only')
+
+ from_id = str(callback_query['from']['id'])
+ reply_msg_id = callback_query['message']['message_id']
+
+ actor = Profile.get(from_id, callback_query)
+ actor['parents'].remove(from_id)
+
+ # удаляем старое сообщение с кнопками
+ r = delete_message(from_id, reply_msg_id)
+ print(r)
+
+ # если ещё есть связи - посылаем новое сообщение
+ if len(actor['parents']) > 0:
+ body = construct_unlink_buttons(actor)
+ r = send_message(from_id, body)
+ print(r)
\ No newline at end of file
diff --git a/tgbot/handlers/callback_vouch.py b/tgbot/handlers/callback_vouch.py
new file mode 100644
index 0000000..91feaa6
--- /dev/null
+++ b/tgbot/handlers/callback_vouch.py
@@ -0,0 +1,38 @@
+from tgbot.api import send_message, forward_message, delete_message
+from tgbot.storage import Profile
+
+
+def handle_button(callback_query):
+ # получаем профиль нажавшего кнопку
+ actor_id = str(callback_query['from']['id'])
+ actor = Profile.get(actor_id, callback_query)
+
+ callback_data = callback_query['data']
+ if callback_data.startswith(BUTTON_VOUCH):
+ print(f'vouch button pressed by {actor_id}')
+
+ newcomer_id = callback_data[len(BUTTON_VOUCH):]
+ newcomer = Profile.get(newcomer_id)
+ if newcomer_id == actor_id:
+ # нажал сам, не реагируем, прописываем данные
+ newcomer = Profile.get(newcomer_id, callback_query)
+ elif newcomer_id not in actor['children'] and \
+ actor_id not in newcomer['parents']:
+ newcomer['parents'].append(actor_id)
+ actor['children'].append(newcomer_id)
+ Profile.save(newcomer)
+ Profile.save(actor)
+ try:
+ chat_id = str(callback_query['message']['chat']['id'])
+
+ print('unmute newcomer')
+ r = unmute_member(chat_id, newcomer_id)
+ print(r)
+
+ print('accept join request')
+ r = approve_chat_join_request(chat_id, newcomer_id)
+ print(r)
+
+ except:
+ pass
+
\ No newline at end of file
diff --git a/tgbot/handlers/command_graph.py b/tgbot/handlers/command_graph.py
new file mode 100644
index 0000000..d757cfb
--- /dev/null
+++ b/tgbot/handlers/command_graph.py
@@ -0,0 +1,29 @@
+from tgbot.utils.graph import generate_chart
+from tgbot.api import send_document
+from tgbot.storage import storage
+import json
+
+
+async def handle_command_graph(msg):
+ cursor = 0
+ keys = []
+ r = storage
+ while True:
+ # Scan for keys starting with 'usr-*' in batches of 100
+ cursor, batch_keys = r.scan(cursor=cursor, match='usr-*', count=100)
+ keys += batch_keys
+ # If the cursor is 0, then we've reached the end of the keys
+ if cursor == 0:
+ break
+ # Get the values of all the keys
+ values = r.mget(keys)
+ # Parse the JSON data from each value
+ members = []
+ for value in values:
+ value_str = value.decode('utf-8')
+ member = json.loads(value_str)
+ members.append(member)
+ print(f'found {len(members)} members')
+ data = generate_chart(members)
+ r = await send_document(msg['chat']['id'], data, 'chart.svg')
+ print(r)
diff --git a/tgbot/handlers/command_my.py b/tgbot/handlers/command_my.py
new file mode 100644
index 0000000..25fe850
--- /dev/null
+++ b/tgbot/handlers/command_my.py
@@ -0,0 +1,40 @@
+from tgbot.storage import Profile
+from tgbot.api import get_member
+
+def construct_unlink_buttons(actor):
+ buttons = []
+ for vouch in actor['children']:
+ vouch_added = False
+ for chat_id in actor['chats']:
+ if not vouch_added:
+ r = get_member(chat_id, vouch)
+ member = r.get('result')
+ if member:
+ try:
+ buttons.append({
+ 'text': mention(r['result']),
+ 'callback_data': 'unlink' + vouch
+ })
+ vouch_added = True
+ except:
+ print('member result error')
+ print(member)
+ else:
+ print(r)
+ return { "inline_keyboard": [ buttons, ] }
+
+def handle_command_my(msg):
+ print(f'handle my command')
+ from_id = str(msg['from']['id'])
+ sender = Profile.get(from_id, msg)
+
+ # генерируем кнопки для всех, за кого поручились
+ reply_markup = construct_unlink_buttons(sender)
+
+ if msg['from'].get('language_code', 'ru') == 'ru':
+ body = 'Нажмите кнопки ниже, чтобы удалить ваши связи'
+ else:
+ body = 'Unlink your connections pressing the buttons below'
+
+ r = send_message(from_id, body, reply_markup=reply_markup)
+ print(r)
\ No newline at end of file
diff --git a/tgbot/handlers/handle_default.py b/tgbot/handlers/handle_default.py
new file mode 100644
index 0000000..9dca22b
--- /dev/null
+++ b/tgbot/handlers/handle_default.py
@@ -0,0 +1,47 @@
+from tgbot.api import send_message, delete_message, get_chat_administrators
+from tgbot.storage import Profile
+from tgbot.handlers.send_button import show_request_msg
+
+
+def handle_default(msg):
+ print(f'default handler for all messages')
+ chat_id = str(msg['chat']['id'])
+ from_id = str(msg['from']['id'])
+ sender = Profile.get(from_id, msg)
+
+ if msg['text'].startswith('/my'):
+ # команда в групповом чате
+ print(f'remove some messages in group chat')
+
+ # удалить сообщение с командой /my
+ r = delete_message(chat_id, msg['message_id'])
+ print(r)
+
+ # удалить предыдушее сообщение с кнопкой в этом чате
+ if sender['request_msg_id'].startswith(chat_id):
+ chat_id, rmid = sender['request_msg_id'].split(':')
+ r = delete_message(chat_id, rmid)
+ print(r)
+
+ # показать новое сообщение с кнопкой
+ request_msg_id = show_request_msg(msg)
+ sender['request_msg_id'] = f'{chat_id}:{request_msg_id}'
+ else:
+ # любое другое сообщение
+ if len(sender['parents']) == 0:
+ # владелец чата автоматически ручается
+ print(f'setting owner as parent for {from_id}')
+ r = get_chat_administrators(chat_id)
+ print(r)
+
+ owner_id = r['result'][0]['id'] # DEBUG!!
+
+ sender['parents'].append(owner_id)
+
+ # обновляем профиль владельца
+ owner = Profile.get(owner_id)
+ owner['children'].append(from_id)
+ Profile.save(owner)
+
+ # сохранить профиль отправителя
+ Profile.save(sender)
\ No newline at end of file
diff --git a/tgbot/handlers/handle_feedback.py b/tgbot/handlers/handle_feedback.py
new file mode 100644
index 0000000..3b8a79e
--- /dev/null
+++ b/tgbot/handlers/handle_feedback.py
@@ -0,0 +1,29 @@
+import json
+
+from tgbot.api import send_message, forward_message, delete_message
+from tgbot.storage import storage, Profile
+from tgbot.config import FEEDBACK_CHAT_ID
+
+
+def handle_feedback(msg):
+ mid = msg['message_id']
+ cid = msg['chat']['id']
+ r = forward_message(cid, mid, FEEDBACK_CHAT_ID)
+ support_msg_id = r['result']['message_id']
+ # сохранение айди сообщения в приватной переписке с ботом
+ storage.set(f'fbk-{support_msg_id}', json.dumps({
+ "author_id": msg["from"]["id"],
+ "message_id": mid,
+ "chat_id": cid
+ }))
+
+
+def handle_answer(msg):
+ print(f'handle answer from support')
+ support_msg_id = str(msg['reply_to_message']['message_id'])
+ # получение сохраненного айди сообщения из личной переписки с ботом
+ stored_feedback = storage.get(f'fbk-{support_msg_id}')
+ stored_feedback = json.loads(stored_feedback)
+ r = send_message(f'{stored_feedback["chat_id"]}', msg['text'], reply_to=stored_feedback["message_id"])
+ print(r)
+
diff --git a/tgbot/handlers/handle_join_request.py b/tgbot/handlers/handle_join_request.py
new file mode 100644
index 0000000..ca60650
--- /dev/null
+++ b/tgbot/handlers/handle_join_request.py
@@ -0,0 +1,26 @@
+from tgbot.api import approve_chat_join_request, delete_message
+from tgbot.handlers.send_button import show_request_msg
+from tgbot.storage import Profile
+
+
+def handle_join_request(msg):
+ print(f'handle join request {msg}')
+ chat_id = str(msg['chat']['id'])
+ from_id = str(msg['from']['id'])
+ actor = Profile.get(from_id, msg)
+
+ if len(actor['parents']) == 0:
+ # показываем сообщение с кнопкой "поручиться"
+ request_msg_id = show_request_msg(msg)
+ # удаляем предыдущее сообщение с кнопкой в этом чате
+ if actor['request_msg_id'].startswith(chat_id):
+ chat_id, rmid = actor['request_msg_id'].split(':')
+ r = delete_message(chat_id, rmid)
+ print(r)
+ actor['request_msg_id'] = f'{chat_id}:{request_msg_id}'
+ else:
+ # за пользователя поручились ранее
+ r = approve_chat_join_request(chat_id, from_id)
+ print(r)
+ Profile.save(actor)
+
diff --git a/tgbot/handlers/handle_members_change.py b/tgbot/handlers/handle_members_change.py
new file mode 100644
index 0000000..85f0412
--- /dev/null
+++ b/tgbot/handlers/handle_members_change.py
@@ -0,0 +1,57 @@
+from tgbot.handlers.send_button import show_request_msg
+from tgbot.api import unmute_member, mute_member, delete_message
+from tgbot.storage import Profile
+
+def handle_join(msg):
+ chat_id = str(msg['chat']['id'])
+ from_id = str(msg['from']['id'])
+
+ actor = Profile.get(from_id, msg)
+
+ newcomer_id = str(msg['new_chat_member']['id'])
+ if from_id == newcomer_id:
+ if len(actor['parents']) == 0:
+ # показываем сообщение с кнопкой "поручиться"
+ request_msg_id = show_request_msg(msg)
+
+ # до одобрения - мьют
+ r = mute_member(chat_id, newcomer_id)
+ print(r)
+
+ # обновляем профиль присоединившегося
+ actor['request_msg_id'] = f'{chat_id}:{request_msg_id}'
+ Profile.save(actor)
+ else:
+ # за пользователя поручились ранее
+ pass
+ else:
+ # пользователи приглашены другим участником
+ print(f'{len(msg["new_chat_members"])} members were invited by {from_id}')
+ for m in msg['new_chat_members']:
+ newcomer = Profile.get(m['id'])
+ newcomer['parents'].append(from_id)
+ Profile.save(newcomer)
+ actor['children'].append(m['id'])
+ r = unmute_member(chat_id, newcomer['id'])
+ print(r)
+
+ # обновляем профиль пригласившего
+ Profile.save(actor)
+
+
+def handle_left(msg):
+ print(f'handling member leaving')
+ member_id = msg["left_chat_member"]["id"]
+ chat_id = msg['chat']['id']
+
+ # профиль покидающего чат
+ leaver = Profile.get(member_id)
+
+ # удаление сообщения с кнопкой в этом чате
+ if leaver['request_msg_id'].startswith(chat_id):
+ chat_id, rmid = leaver['request_msg_id'].split(':')
+ r = delete_message(chat_id, rmid)
+ print(r)
+
+ Profile.leaving(leaver)
+
diff --git a/tgbot/handlers/send_button.py b/tgbot/handlers/send_button.py
new file mode 100644
index 0000000..a6a20ac
--- /dev/null
+++ b/tgbot/handlers/send_button.py
@@ -0,0 +1,27 @@
+from tgbot.api import send_message
+from tgbot.config import BUTTON_VOUCH, NEWCOMER_MSG
+from tgbot.utils import mention
+
+def show_request_msg(msg):
+ reply_markup = {
+ "inline_keyboard": [
+ [
+ {
+ "text": BUTTON_VOUCH,
+ "callback_data": BUTTON_VOUCH + str(msg['from']['id'])
+ }
+ ]
+ ]
+ }
+
+ r = send_message(
+ msg['chat']['id'],
+ NEWCOMER_MSG + mention(msg['from']),
+ reply_to=msg.get('message_id', ''),
+ reply_markup=reply_markup
+ )
+ request_msg_id = r['result']['message_id']
+ chat_id = msg['chat']['id']
+ print(r)
+ print(f'request message id: {chat_id}:{request_msg_id}')
+ return request_msg_id
diff --git a/tgbot/profile.py b/tgbot/profile.py
deleted file mode 100644
index 0339bf8..0000000
--- a/tgbot/profile.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import json
-
-
-class Profile:
-
- def __init__(self, storage):
- self.storage = storage
-
- def create(self, member_id):
- s = {
- "id": member_id,
- "name": "newcomer",
- "mention": "",
- "welcome_id": 0,
- "parents": [],
- "children": []
- }
- self.storage.set(f'usr-{member_id}', json.dumps(s))
- return s
-
- def save(self, s):
- self.storage.set(f'usr-{s["id"]}', json.dumps(s))
-
- def get(self, member_id):
- data = self.storage.get(f'usr-{member_id}')
- if data is None:
- return self.create(member_id)
- else:
- return json.loads(data)
-
- def leaving(self, s):
- if len(s['parents']) == 0:
- self.storage.delete(f'usr-{s["id"]}')
diff --git a/tgbot/storage/__init__.py b/tgbot/storage/__init__.py
new file mode 100644
index 0000000..9aba69f
--- /dev/null
+++ b/tgbot/storage/__init__.py
@@ -0,0 +1,9 @@
+import redis
+from tgbot.storage.profile import Profile as ProfileObj
+from tgbot.config import REDIS_URL
+
+# сохраняет сессии и пересылаемые сообщения между перезагрузками
+storage = redis.from_url(REDIS_URL)
+
+# хранение необходимой информации о пользователях
+Profile = ProfileObj(storage)
diff --git a/tgbot/storage/profile.py b/tgbot/storage/profile.py
new file mode 100644
index 0000000..5b0fc77
--- /dev/null
+++ b/tgbot/storage/profile.py
@@ -0,0 +1,44 @@
+import json
+
+
+class Profile:
+
+ def __init__(self, storage):
+ self.storage = storage
+
+ def create(self, member_id, msg=None):
+ s = {
+ "id": member_id,
+ "request_msg_id": 0,
+ "parents": [],
+ "children": [],
+ "chats": []
+ }
+
+ if msg.get('from'):
+ sender = msg.get('from')
+ s["mention"] = sender.get('username')
+ s["name"] = f"{sender['first_name']} {sender.get('last_name', '')}".strip()
+
+ if msg.get('chat'):
+ chat_id = str(msg['chat']['id'])
+ if chat_id not in s['chats']:
+ s["chats"].append(chat_id)
+
+ self.storage.set(f'usr-{member_id}', json.dumps(s))
+ return s
+
+ def save(self, s):
+ self.storage.set(f'usr-{s["id"]}', json.dumps(s))
+
+ def get(self, member_id, msg=None):
+ data = self.storage.get(f'usr-{member_id}')
+ if data is None:
+ r = self.create(member_id, msg)
+ else:
+ r = json.loads(data)
+ return r
+
+ def leaving(self, s):
+ if len(s['parents']) == 0:
+ self.storage.delete(f'usr-{s["id"]}')
diff --git a/tgbot/utils/graph.py b/tgbot/utils/graph.py
new file mode 100644
index 0000000..866bd67
--- /dev/null
+++ b/tgbot/utils/graph.py
@@ -0,0 +1,67 @@
+# Define SVG code generation function with member_id parameter
+def generate_chart(members, member_id = None):
+ if not member_id:
+ member_id = members[0]['id']
+ # Define some constants for layout
+ node_radius = 30
+ node_spacing = 80
+ node_y = 100
+ parent_y_offset = 50
+ child_y_offset = 150
+
+ # Find the specified member and its descendants
+ member = None
+ descendants = set()
+ for m in members:
+ if m["id"] == member_id:
+ member = m
+ descendants.add(member_id)
+ break
+
+ stack = member["children"].copy()
+ while stack:
+ child_id = stack.pop()
+ descendants.add(child_id)
+ for m in members:
+ if m["id"] == child_id:
+ stack.extend(m["children"])
+ break
+
+ # Define the x position for each member
+ x_positions = {}
+ x_positions[member_id] = 0
+ for i, m in enumerate(members):
+ if m["id"] in descendants:
+ x_positions[m["id"]] = (i * node_spacing) + node_radius
+
+ # Start building the SVG code
+ svg_width = (len(descendants) * node_spacing) + (2 * node_radius)
+ svg_height = 200
+ svg_code = f''
+
+ return svg_code.encode('utf-8')
\ No newline at end of file
diff --git a/tgbot/utils/mention.py b/tgbot/utils/mention.py
new file mode 100644
index 0000000..4e85c1e
--- /dev/null
+++ b/tgbot/utils/mention.py
@@ -0,0 +1,9 @@
+# generates a mention from standard telegram web json 'from' field
+# using HTML markup
+def mention(user):
+ identity = f"{user['first_name']} {user.get('last_name', '')}".strip()
+ uid = user['id']
+ username = user.get('username', '')
+ if username:
+ username = f'(@{username})'
+ return f'{identity}{username}'