0.0.8-newage
This commit is contained in:
parent
297445fd50
commit
7b12a491e8
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,7 +1,16 @@
|
|||
## [0.0.8]
|
||||
|
||||
- генерация древовидного графа, с опорой на одного участника
|
||||
- /my для просмотра и изменения связей
|
||||
- возможность отмены поручительства
|
||||
- рефакторинг
|
||||
|
||||
|
||||
## [0.0.7]
|
||||
|
||||
- исправления
|
||||
- команда, генерирующая граф связей
|
||||
- мьют на входе, там где заявки не включены
|
||||
- одобрение заявки: любой участник может поручиться
|
||||
- за всех кто уже в чате и пишет сообщения
|
||||
|
||||
|
||||
## [0.0.6]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
sanic==19.6.0
|
||||
requests
|
||||
redis
|
||||
cairosvg
|
||||
aiohttp
|
80
tgbot/api.py
80
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()
|
||||
|
||||
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()
|
|
@ -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'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{line_color}" stroke-width="2"/>')
|
||||
|
||||
# Рисуем узлы
|
||||
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'<rect x="{x}" y="{y}" width="{node_width}" height="{node_height}" rx="{node_radius}" fill="{node_color}" stroke="{node_stroke_color}" stroke-width="2"/>')
|
||||
|
||||
# Добавляем текст в центр узла
|
||||
member_name = member['name'][:16]
|
||||
text_x = x + node_width / 2
|
||||
text_y = y + node_height / 2
|
||||
svg_lines.append(f'<text x="{text_x}" y="{text_y}" text-anchor="middle" dominant-baseline="central" font-size="16" fill="{node_text_color}">{member_name}</text>')
|
||||
|
||||
# Создаем SVG-код
|
||||
svg = f'<svg viewBox="0 0 {canvas_width} {canvas_height}" xmlns="http://www.w3.org/2000/svg" style="background-color:{background_color};">'
|
||||
for line in svg_lines:
|
||||
svg += line
|
||||
svg += '</svg>'
|
||||
# конвертировать SVG в PNG
|
||||
png_data = cairosvg.svg2png(bytestring=svg_data)
|
||||
return png_data
|
|
@ -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())
|
23
tgbot/handlers/callback_unlink.py
Normal file
23
tgbot/handlers/callback_unlink.py
Normal file
|
@ -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)
|
38
tgbot/handlers/callback_vouch.py
Normal file
38
tgbot/handlers/callback_vouch.py
Normal file
|
@ -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
|
||||
|
29
tgbot/handlers/command_graph.py
Normal file
29
tgbot/handlers/command_graph.py
Normal file
|
@ -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)
|
40
tgbot/handlers/command_my.py
Normal file
40
tgbot/handlers/command_my.py
Normal file
|
@ -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)
|
47
tgbot/handlers/handle_default.py
Normal file
47
tgbot/handlers/handle_default.py
Normal file
|
@ -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)
|
29
tgbot/handlers/handle_feedback.py
Normal file
29
tgbot/handlers/handle_feedback.py
Normal file
|
@ -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)
|
||||
|
26
tgbot/handlers/handle_join_request.py
Normal file
26
tgbot/handlers/handle_join_request.py
Normal file
|
@ -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)
|
||||
|
57
tgbot/handlers/handle_members_change.py
Normal file
57
tgbot/handlers/handle_members_change.py
Normal file
|
@ -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)
|
||||
|
27
tgbot/handlers/send_button.py
Normal file
27
tgbot/handlers/send_button.py
Normal file
|
@ -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
|
|
@ -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"]}')
|
9
tgbot/storage/__init__.py
Normal file
9
tgbot/storage/__init__.py
Normal file
|
@ -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)
|
44
tgbot/storage/profile.py
Normal file
44
tgbot/storage/profile.py
Normal file
|
@ -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"]}')
|
67
tgbot/utils/graph.py
Normal file
67
tgbot/utils/graph.py
Normal file
|
@ -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'<svg width="{svg_width}" height="{svg_height}">'
|
||||
|
||||
# Generate nodes and names for each descendant
|
||||
for m in members:
|
||||
if m["id"] in descendants:
|
||||
node_x = x_positions[m["id"]]
|
||||
node_code = f'<circle cx="{node_x}" cy="{node_y}" r="{node_radius}" stroke="black" stroke-width="2" fill="white"/>'
|
||||
name_code = f'<text x="{node_x}" y="{node_y}" font-size="16" text-anchor="middle">{m["name"]}</text>'
|
||||
svg_code += node_code + name_code
|
||||
|
||||
# Generate links to parent nodes
|
||||
for parent_id in m["parents"]:
|
||||
if parent_id in descendants:
|
||||
parent_x = x_positions[parent_id]
|
||||
link_code = f'<line x1="{node_x}" y1="{node_y - parent_y_offset}" x2="{parent_x}" y2="{node_y}" stroke="black" stroke-width="2"/>'
|
||||
svg_code += link_code
|
||||
|
||||
# Generate links to child nodes
|
||||
for child_id in m["children"]:
|
||||
if child_id in descendants:
|
||||
child_x = x_positions[child_id]
|
||||
link_code = f'<line x1="{node_x}" y1="{node_y + child_y_offset}" x2="{child_x}" y2="{node_y}" stroke="black" stroke-width="2"/>'
|
||||
svg_code += link_code
|
||||
|
||||
# Finish the SVG code
|
||||
svg_code += '</svg>'
|
||||
|
||||
return svg_code.encode('utf-8')
|
9
tgbot/utils/mention.py
Normal file
9
tgbot/utils/mention.py
Normal file
|
@ -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'<a href="tg://user?id={uid}"><b>{identity}</b></a>{username}'
|
Loading…
Reference in New Issue
Block a user