Пример Instance бота, разные команды

This commit is contained in:
mihalin 2021-06-29 15:29:41 +03:00
parent 4e70ebf32c
commit d738da2fa9
15 changed files with 221 additions and 33 deletions

View File

@ -5,7 +5,7 @@ Open-source self-hosted Livegram alternative
##### #####
таблицы instance поведение
Кто-то написал сообщение в любом чате - переслать в супер-чат
Боты: токен, имя, владелец Кто-то ответил на сообщение в супер-чате - переслать автору сообщения
Пользователи: идентификатор Кто-то написал /start - отправить стартовое сообщение

3
bot.py
View File

@ -6,7 +6,7 @@ from settings import BotSettings
from olgram.bot.bots import router as bots_router from olgram.bot.bots import router as bots_router
from olgram.bot.start import router as start_router from olgram.bot.start import router as start_router
from olgram.bot.bot import router as bot_router
from olgram.utils.database import init_database from olgram.utils.database import init_database
@ -21,6 +21,7 @@ def main():
start_router.setup(dp) start_router.setup(dp)
bots_router.setup(dp) bots_router.setup(dp)
bot_router.setup(dp)
executor.start_polling(dp, skip_updates=True) executor.start_polling(dp, skip_updates=True)

View File

@ -7,9 +7,18 @@ services:
- POSTGRES_PASSWORD=test_passwd - POSTGRES_PASSWORD=test_passwd
- POSTGRES_DB=olgram - POSTGRES_DB=olgram
ports: ports:
- '5431:5432' - '5430:5432'
volumes: volumes:
- database:/var/lib/postgresql/data - database:/var/lib/postgresql/data
redis:
image: 'bitnami/redis:latest'
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- redis-db:/bitnami/redis/data
ports:
- '6370:6379'
volumes: volumes:
database: database:
redis-db:

10
instance/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3.8-buster
COPY . /app
WORKDIR /app
RUN pip install --upgrade pip && \
pip install -r requirements.txt && \
CMD ["python", "instance"]

0
instance/__init__.py Normal file
View File

77
instance/bot.py Normal file
View File

@ -0,0 +1,77 @@
import asyncio
import aioredis
import typing as ty
from aiogram import Bot, Dispatcher, executor, types, exceptions
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from olgram.utils.router import Router
token = "(token)"
bot_id = token.split(":")[0]
start_text = 'Здравствуйте! Напишите тут что-то'
super_chat_id = -1 # ID чата здесь
router = Router()
redis: ty.Optional[aioredis.Redis] = None
def message_unique_id(message_id) -> str:
return bot_id + "-" + str(message_id)
@router.message_handler(content_types=[types.ContentType.ANY])
async def receive_text(message: types.Message):
"""
Some text received
:param message:
:return:
"""
if message.text and message.text.startswith("/start"):
await message.answer(start_text)
return
if message.chat.id != super_chat_id:
# Это обычный чат
new_message = await message.forward(super_chat_id)
await redis.set(message_unique_id(new_message.message_id), message.chat.id)
else:
# Это чат, в который бот должен пересылать сообщения
if message.reply_to_message:
chat_id = await redis.get(message_unique_id(message.reply_to_message.message_id))
if not chat_id:
chat_id = message.reply_to_message.forward_from_chat
if not chat_id:
await message.reply("Невозможно ответить, автор сообщения не найден")
return
chat_id = int(chat_id)
try:
await message.copy_to(chat_id)
except exceptions.MessageError:
await message.reply("Невозможно отправить сообщение пользователю: возможно, он заблокировал бота")
return
else:
await message.forward(super_chat_id)
async def init_redis():
global redis
redis = await aioredis.create_redis_pool('redis://localhost:6370')
def main():
"""
Classic polling
"""
asyncio.get_event_loop().run_until_complete(init_redis())
bot = Bot(token)
dp = Dispatcher(bot, storage=MemoryStorage())
router.setup(dp)
executor.start_polling(dp, skip_updates=True)
if __name__ == '__main__':
main()

46
olgram/bot/bot.py Normal file
View File

@ -0,0 +1,46 @@
from aiogram import types, Bot as AioBot
from aiogram.dispatcher import FSMContext
from aiogram.utils.callback_data import CallbackData
from textwrap import dedent
from olgram.utils.router import Router
from olgram.utils.mix import try_delete_message
from olgram.models.models import Bot, User
router = Router()
# Пользователь выбрал бота
select_bot = CallbackData('bot_select', 'bot_id')
# Пользователь выбрал, что хочет сделать со своим ботом
bot_operation = CallbackData('bot_operation', 'operation')
@router.callback_query_handler(select_bot.filter())
async def select_bot_callback(call: types.CallbackQuery, callback_data: dict, state: FSMContext):
"""
Пользователь выбрал бота для редактирования
"""
bot_id = callback_data["bot_id"]
bot = await Bot.get_or_none(id=bot_id)
if not bot or (await bot.owner).telegram_id != call.from_user.id:
await call.answer("Такого бота нет", show_alert=True)
return
async with state.proxy() as proxy:
proxy["bot"] = bot
await try_delete_message(call.message)
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(types.InlineKeyboardButton(text="Текст", callback_data=bot_operation.new(operation="text")))
keyboard.insert(types.InlineKeyboardButton(text="Чат", callback_data=bot_operation.new(operation="chat")))
keyboard.insert(types.InlineKeyboardButton(text="Удалить бот", callback_data=bot_operation.new(operation="delete")))
keyboard.insert(types.InlineKeyboardButton(text="<<Вернуться к списку ботов",
callback_data=bot_operation.new(operation="back")))
await AioBot.get_current().send_message(call.message.chat.id, dedent(f"""
Управление ботом @{bot.name}.
Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help.
"""), reply_markup=keyboard)

View File

@ -5,40 +5,65 @@ import re
from textwrap import dedent from textwrap import dedent
from ..utils.router import Router from ..utils.router import Router
from olgram.models.bot import Bot from .bot import select_bot
from olgram.models.user import User from olgram.models.models import Bot, User
from olgram.settings import OlgramSettings
router = Router() router = Router()
token_pattern = r'[0-9]{8,10}:[a-zA-Z0-9_-]{35}' token_pattern = r'[0-9]{8,10}:[a-zA-Z0-9_-]{35}'
@router.message_handler(commands=["my_bots"]) @router.message_handler(commands=["mybots"], state="*")
async def my_bots(message: types.Message, state: FSMContext): async def my_bots(message: types.Message, state: FSMContext):
"""
Команда /mybots (список ботов)
"""
user = await User.get_or_none(telegram_id=message.from_user.id) user = await User.get_or_none(telegram_id=message.from_user.id)
bots = await Bot.filter(owner=user) bots = await Bot.filter(owner=user)
if not bots: if not bots:
await message.answer(dedent(""" await message.answer(dedent("""
У вас нет добавленных ботов. У вас нет добавленных ботов.
Отправьте команду /add_bot, чтобы добавить бот. Отправьте команду /addbot, чтобы добавить бот.
""")) """))
return return
bots_str = "\n".join(["@" + bot.name for bot in bots]) keyboard = types.InlineKeyboardMarkup(row_width=2)
await message.answer(dedent(f""" for bot in bots:
Ваши боты: keyboard.insert(types.InlineKeyboardButton(text="@" + bot.name, callback_data=select_bot.new(bot_id=bot.id)))
{bots_str}
""")) await message.answer("Ваши боты", reply_markup=keyboard)
@router.message_handler(commands=["add_bot"]) @router.message_handler(commands=["addbot"], state="*")
async def add_bot(message: types.Message, state: FSMContext): async def add_bot(message: types.Message, state: FSMContext):
await message.answer("Окей, пришли тогда токен plz") """
Команда /addbot (добавить бота)
"""
bot_count = await Bot.filter(user__telegram_id=message.from_user.id).count()
if bot_count > OlgramSettings.max_bots_per_user():
await message.answer("У вас уже слишком много ботов")
return
await message.answer(dedent("""
Чтобы подключить бот, вам нужно выполнить три действия:
1. Перейдите в бот @BotFather, нажмите START и отправьте команду /newbot
2. Введите название бота, а потом username бота.
3. После создания бота перешлите ответное сообщение в этот бот или скопируйте и пришлите token бота.
Важно: не подключайте боты, которые используются в других сервисах (Manybot, Chatfuel, Livegram и других).
Подробную инструкцию по созданию бота читайте здесь /help)
"""))
await state.set_state("add_bot") await state.set_state("add_bot")
@router.message_handler(state="add_bot", content_types="text", regexp="^[^/].+") # Not command @router.message_handler(state="add_bot", content_types="text", regexp="^[^/].+") # Not command
async def bot_added(message: types.Message, state: FSMContext): async def bot_added(message: types.Message, state: FSMContext):
"""
Пользователь добавляет бота и мы ждём от него токен
"""
token = re.findall(token_pattern, message.text) token = re.findall(token_pattern, message.text)
async def on_invalid_token(): async def on_invalid_token():
@ -66,6 +91,7 @@ async def bot_added(message: types.Message, state: FSMContext):
try: try:
test_bot = AioBot(token) test_bot = AioBot(token)
test_bot_info = await test_bot.get_me() test_bot_info = await test_bot.get_me()
await test_bot.session.close()
except ValueError: except ValueError:
return await on_invalid_token() return await on_invalid_token()
except Unauthorized: except Unauthorized:

View File

@ -1,4 +1,4 @@
from aiogram import Bot, Dispatcher, executor, types from aiogram import types
from aiogram.dispatcher import FSMContext from aiogram.dispatcher import FSMContext
from textwrap import dedent from textwrap import dedent
from ..utils.router import Router from ..utils.router import Router
@ -9,7 +9,7 @@ router = Router()
@router.message_handler(commands=["start"], state="*") @router.message_handler(commands=["start"], state="*")
async def start(message: types.Message, state: FSMContext): async def start(message: types.Message, state: FSMContext):
""" """
Start command handler Команда /start
""" """
await state.reset_state() await state.reset_state()
@ -20,8 +20,8 @@ async def start(message: types.Message, state: FSMContext):
Используйте эти команды, чтобы управлять этим ботом: Используйте эти команды, чтобы управлять этим ботом:
/add_bot - добавить бот /addbot - добавить бот
/my_bots - управление ботами /mybots - управление ботами
/help - помощь /help - помощь
@ -32,7 +32,7 @@ async def start(message: types.Message, state: FSMContext):
@router.message_handler(commands=["help"], state="*") @router.message_handler(commands=["help"], state="*")
async def help(message: types.Message, state: FSMContext): async def help(message: types.Message, state: FSMContext):
""" """
Help command handler Команда /help
""" """
await message.answer(dedent(""" await message.answer(dedent("""
<todo: help here> <todo: help here>

View File

@ -10,3 +10,11 @@ class Bot(Model):
class Meta: class Meta:
table = 'bot' table = 'bot'
class User(Model):
id = fields.IntField(pk=True)
telegram_id = fields.IntField(index=True, unique=True)
class Meta:
table = 'user'

View File

@ -1,9 +0,0 @@
from tortoise import Model, fields
class User(Model):
id = fields.IntField(pk=True)
telegram_id = fields.IntField(index=True, unique=True)
class Meta:
table = 'user'

View File

@ -15,6 +15,16 @@ class _Settings(ABC):
return parameter return parameter
class OlgramSettings(_Settings):
@classmethod
def max_bots_per_user(cls) -> int:
"""
Максимальное количество ботов у одного пользователя
:return: int
"""
return 5
class BotSettings(_Settings): class BotSettings(_Settings):
@classmethod @classmethod
def token(cls) -> str: def token(cls) -> str:

View File

@ -8,8 +8,8 @@ async def init_database():
# which contain models from "app.models" # which contain models from "app.models"
await Tortoise.init( await Tortoise.init(
db_url=f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' db_url=f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}'
f'@localhost:5431/{DatabaseSettings.database_name()}', f'@localhost:5430/{DatabaseSettings.database_name()}',
modules={'models': ['olgram.models.bot', 'olgram.models.user']} modules={'models': ['olgram.models.models']}
) )
# Generate the schema # Generate the schema
await Tortoise.generate_schemas() await Tortoise.generate_schemas()

9
olgram/utils/mix.py Normal file
View File

@ -0,0 +1,9 @@
from aiogram.types import Message
from aiogram.utils.exceptions import TelegramAPIError
async def try_delete_message(message: Message):
try:
await message.delete()
except TelegramAPIError:
pass

View File

@ -2,3 +2,4 @@ aiogram
tortoise-orm[asyncpg] tortoise-orm[asyncpg]
aerich aerich
python-dotenv python-dotenv
aioredis