From 4b6a211c093a920c24a43530b0f88dc32ed1db2f Mon Sep 17 00:00:00 2001 From: Cathy Hu Date: Fri, 10 Jul 2020 23:53:24 +0200 Subject: [PATCH] [telegram] Add telegram bot --- kibicara/platforms/telegram/__init__.py | 0 kibicara/platforms/telegram/bot.py | 89 +++++++++++++++++++++++++ kibicara/platforms/telegram/model.py | 26 ++++++++ kibicara/platforms/telegram/webapi.py | 59 ++++++++++++++++ kibicara/webapi/__init__.py | 4 ++ 5 files changed, 178 insertions(+) create mode 100644 kibicara/platforms/telegram/__init__.py create mode 100644 kibicara/platforms/telegram/bot.py create mode 100644 kibicara/platforms/telegram/model.py create mode 100644 kibicara/platforms/telegram/webapi.py diff --git a/kibicara/platforms/telegram/__init__.py b/kibicara/platforms/telegram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kibicara/platforms/telegram/bot.py b/kibicara/platforms/telegram/bot.py new file mode 100644 index 0000000..4b6d120 --- /dev/null +++ b/kibicara/platforms/telegram/bot.py @@ -0,0 +1,89 @@ +# Copyright (C) 2020 by Cathy Hu +# +# SPDX-License-Identifier: 0BSD + +from aiogram import Bot, Dispatcher, exceptions, types +from asyncio import gather, sleep +from kibicara.config import config +from kibicara.platformapi import Censor, Message, Spawner +from kibicara.platforms.telegram.model import Telegram, TelegramUser +from logging import getLogger + + +logger = getLogger(__name__) + + +class TelegramBot(Censor): + def __init__(self, telegram_model): + super().__init__(telegram_model.hood) + self.telegram_model = telegram_model + self.bot = Bot(token=telegram_model.api_token) + self.dp = Dispatcher(self.bot) + self.dp.register_message_handler(self._send_welcome, commands=['start']) + self.dp.register_message_handler(self._receive_message) + + async def run(self): + await gather(self.dp.start_polling(), self.push()) + + async def push(self): + while True: + message = await self.receive() + logger.debug( + 'Received message from censor (%s): %s' + % (self.telegram_model.hood.name, message.text) + ) + for user in await TelegramUser.objects.filter( + bot=self.telegram_model + ).all(): + await self._send_message(user.user_id, message.text) + + async def _send_message(self, user_id, message): + try: + await self.bot.send_message(user_id, message, disable_notification=False) + except exceptions.BotBlocked: + logger.error( + 'Target [ID:%s] (%s): blocked by user' + % (user_id, self.telegram_model.hood.name) + ) + except exceptions.ChatNotFound: + logger.error( + 'Target [ID:%s] (%s): invalid user ID' + % (user_id, self.telegram_model.hood.name) + ) + except exceptions.RetryAfter as e: + logger.error( + 'Target [ID:%s] (%s): Flood limit is exceeded. Sleep %d seconds.' + % (user_id, self.telegram_model.hood.name, e.timeout) + ) + await sleep(e.timeout) + return await self._send_message(user_id, text) + except exceptions.UserDeactivated: + logger.error( + 'Target [ID:%s] (%s): user is deactivated' + % (user_id, self.telegram_model.hood.name) + ) + except exceptions.TelegramAPIError: + logger.exception( + 'Target [ID:%s] (%s): failed' % (user_id, self.telegram_model.hood.name) + ) + + async def _send_welcome(self, message: types.Message): + try: + if message.from_user.is_bot: + await message.reply('Error: Bots can not join here.') + return + await TelegramUser.objects.create( + user_id=message.from_user.id, bot=self.telegram_model + ) + await message.reply(self.telegram_model.welcome_message) + except IntegrityError: + await message.reply('Error: You are already registered.') + + async def _receive_message(self, message: types.Message): + if not message.text: + await message.reply('Error: Only text messages are allowed.') + return + await self.publish(Message(message.text)) + + +spawner = Spawner(Telegram, TelegramBot) diff --git a/kibicara/platforms/telegram/model.py b/kibicara/platforms/telegram/model.py new file mode 100644 index 0000000..7fbc9e4 --- /dev/null +++ b/kibicara/platforms/telegram/model.py @@ -0,0 +1,26 @@ +# Copyright (C) 2020 by Cathy Hu +# +# SPDX-License-Identifier: 0BSD + +from kibicara.model import Hood, Mapping +from ormantic import Boolean, Integer, ForeignKey, Model, Text + + +class Telegram(Model): + id: Integer(primary_key=True) = None + hood: ForeignKey(Hood) + api_token: Text() + welcome_message: Text() + + class Mapping(Mapping): + table_name = 'telegrambots' + + +class TelegramUser(Model): + id: Integer(primary_key=True) = None + user_id: Integer(unique=True) + # TODO unique + bot: ForeignKey(Telegram) + + class Mapping(Mapping): + table_name = 'telegramusers' diff --git a/kibicara/platforms/telegram/webapi.py b/kibicara/platforms/telegram/webapi.py new file mode 100644 index 0000000..21140ee --- /dev/null +++ b/kibicara/platforms/telegram/webapi.py @@ -0,0 +1,59 @@ +# Copyright (C) 2020 by Cathy Hu +# +# SPDX-License-Identifier: 0BSD + +from fastapi import APIRouter, Depends, HTTPException, Response, status +from kibicara.config import config +from kibicara.platforms.telegram.bot import spawner +from kibicara.platforms.telegram.model import Telegram +from kibicara.webapi.hoods import get_hood +from logging import getLogger +from sqlite3 import IntegrityError +from ormantic.exceptions import NoMatch +from pydantic import BaseModel + + +logger = getLogger(__name__) + + +class BodyTelegram(BaseModel): + api_token: str + welcome_message: str = 'Welcome!' + + +async def get_telegram(telegram_id: int, hood=Depends(get_hood)): + try: + return await Telegram.objects.get(id=telegram_id, hood=hood) + except NoMatch: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + +router = APIRouter() +telegram_callback_router = APIRouter() + + +@router.get('/') +async def telegram_read_all(hood=Depends(get_hood)): + return await Telegram.objects.filter(hood=hood).all() + + +@router.get('/{telegram_id}') +async def telegram_read(telegram=Depends(get_telegram)): + return telegram + + +@router.delete('/{telegram_id}', status_code=status.HTTP_204_NO_CONTENT) +async def telegram_delete(telegram=Depends(get_telegram)): + spawner.stop(telegram) + await telegram.delete() + + +@router.post('/', status_code=status.HTTP_201_CREATED) +async def telegram_create(values: BodyTelegram, hood=Depends(get_hood)): + try: + telegram = await Telegram.objects.create(hood=hood, **values.__dict__) + spawner.start(telegram) + response.headers['Location'] = '%d' % telegram.id + return telegram + except IntegrityError: + raise HTTPException(status_code=status.HTTP_409_CONFLICT) diff --git a/kibicara/webapi/__init__.py b/kibicara/webapi/__init__.py index 53f51fc..cdeef5b 100644 --- a/kibicara/webapi/__init__.py +++ b/kibicara/webapi/__init__.py @@ -5,6 +5,7 @@ from fastapi import APIRouter from kibicara.platforms.test.webapi import router as test_router +from kibicara.platforms.telegram.webapi import router as telegram_router from kibicara.platforms.twitter.webapi import router as twitter_router from kibicara.platforms.twitter.webapi import twitter_callback_router from kibicara.webapi.admin import router as admin_router @@ -18,6 +19,9 @@ router.include_router(admin_router, prefix='/admin', tags=['admin']) hoods_router.include_router(triggers_router, prefix='/{hood_id}/triggers') hoods_router.include_router(badwords_router, prefix='/{hood_id}/badwords') hoods_router.include_router(test_router, prefix='/{hood_id}/test', tags=['test']) +hoods_router.include_router( + telegram_router, prefix='/{hood_id}/telegram', tags=['telegram'] +) hoods_router.include_router( twitter_router, prefix='/{hood_id}/twitter', tags=['twitter'] )