Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor default command to accept raw text #24

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Development Lead
Contributors
------------

None yet. Why not be the first?
* Frédéric Massart (FMCorz)
8 changes: 8 additions & 0 deletions docs/telegrambot.bot_views.generic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ telegrambot.bot_views.generic.list module
:undoc-members:
:show-inheritance:

telegrambot.bot_views.generic.message module
--------------------------------------------

.. automodule:: telegrambot.bot_views.generic.message
:members:
:undoc-members:
:show-inheritance:

telegrambot.bot_views.generic.responses module
----------------------------------------------

Expand Down
24 changes: 20 additions & 4 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ in settings and with it correct value in the DB. The webhook for each bot is set
``enabled`` field is set to true.


Bot views responses with Telegram messages to the user with a text message and keyboard.
Compound with a context and a template. The way it is handled is analogue to Django views.
The bot views inheriting from ``SendMessageCommandView`` respond with Telegram messages
including a text message and keyboard. Defining a bot view is really easy using generic
classed views, analogues to django generic views. Alternatively, if you need to respond
with a simple message, you can init ``SendMessageCommandView`` just like so::

Define a bot view is really easy using generic classed views, analogues to django generic views.
urlpatterns = [
...
command('/say_hi', SendMessageCommandView.as_command_view(message='Hi there!'))
...
]

Simple view just with a template, image /start command just to wellcome::
A simple view just based on a template, image /start command just to welcome::

class StartView(TemplateCommandView):
template_text = "bot/messages/command_start_text.txt"
Expand Down Expand Up @@ -75,6 +81,16 @@ Templates works just as normal django app. In /start command example it will sea
for ``bot/messages/command_start_text.txt`` to compound response message and
``bot/messages/command_start_keyboard.txt``.

For testing, you can use the ``EchoCommandView`` and ``HelloWorldCommandView`` views::

from telegrambot.bot_views.generic import EchoCommandView, HelloWorldCommandView
from telegrambot.handlers import unknown_command, message

urlpatterns = [
unknown_command(HelloWorldCommandView.as_command_view()),
message(EchoCommandView.as_command_view())
]

Authentication
-------------------------

Expand Down
3 changes: 2 additions & 1 deletion telegrambot/bot_views/generic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from telegrambot.bot_views.generic.base import TemplateCommandView # noqa
from telegrambot.bot_views.generic.message import (SendMessageCommandView, EchoCommandView, # noqa
HelloWorldCommandView, TemplateCommandView)
from telegrambot.bot_views.generic.compound import ListDetailCommandView # noqa
from telegrambot.bot_views.generic.detail import DetailCommandView # noqa
from telegrambot.bot_views.generic.list import ListCommandView # noqa
Expand Down
81 changes: 50 additions & 31 deletions telegrambot/bot_views/generic/base.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
from telegrambot.bot_views.generic.responses import TextResponse, KeyboardResponse
from telegram import ParseMode
import sys
import traceback
import logging

logger = logging.getLogger(__name__)
PY3 = sys.version_info > (3,)

class TemplateCommandView(object):
template_text = None
template_keyboard = None

def get_context(self, bot, update, **kwargs):
return None

def handle(self, bot, update, **kwargs):
try:
ctx = self.get_context(bot, update, **kwargs)
text = TextResponse(self.template_text, ctx).render()
keyboard = KeyboardResponse(self.template_keyboard, ctx).render()
# logger.debug("Text:" + str(text.encode('utf-8')))
# logger.debug("Keyboard:" + str(keyboard))
if text:
if not PY3:
text = text.encode('utf-8')
bot.send_message(chat_id=update.message.chat_id, text=text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
else:
logger.info("No text response for update %s" % str(update))
except:
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
raise


class BaseCommandView(object):
"""Base Command View.

This base class defines the handle method which you should implement
in sub classes. The parameters you care about are accessible through
the properties bot, update and kwargs. Note that the latter are not
available in the constructor.
"""

_bot = None
_update = None
_kwargs = None

def handle(self, *args, **kwargs):
pass

def init(self, bot, update, **kwargs):
"""Init the view with the handling arguments.

We could have done this in the constructor, but to maintain backwards compatibility with classes
which did not call super in the constructor, we do this separately. This also simplifies the
implementation of a subclass as a super call to the parent constructor is not required.
"""

self._bot = bot
self._update = update
self._kwargs = kwargs

@property
def bot(self):
return self._bot

@property
def update(self):
return self._update

@property
def kwargs(self):
return self._kwargs

@classmethod
def as_command_view(cls, **initkwargs):
def as_command_view(cls, *initargs, **initkwargs):
def view(bot, update, **kwargs):
self = cls(**initkwargs)
return self.handle(bot, update, **kwargs)
try:
self = cls(*initargs, **initkwargs)
self.init(bot, update, **kwargs)
return self.handle()
except:
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
raise
return view
20 changes: 12 additions & 8 deletions telegrambot/bot_views/generic/compound.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from telegrambot.bot_views.generic.base import TemplateCommandView
from telegrambot.bot_views.generic.base import BaseCommandView

class ListDetailCommandView(TemplateCommandView):
class ListDetailCommandView(BaseCommandView):
list_view_class = None
detail_view_class = None

@classmethod
def as_command_view(cls, **initkwargs):
def as_command_view(cls, *initargs, **initkwargs):
def view(bot, update, **kwargs):
command_args = update.message.text.split(' ')
args = []

if len(command_args) > 1:
self = cls.detail_view_class(command_args[1])
class_ = cls.detail_view_class
args.append(command_args[1])
else:
self = cls.list_view_class()
return self.handle(bot, update, **kwargs)
return view
class_ = cls.list_view_class

return class_.as_command_view(*args)(bot, update, **kwargs)
return view
2 changes: 1 addition & 1 deletion telegrambot/bot_views/generic/detail.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from telegrambot.bot_views.generic.base import TemplateCommandView
from telegrambot.bot_views.generic.message import TemplateCommandView
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist,\
FieldError

Expand Down
2 changes: 1 addition & 1 deletion telegrambot/bot_views/generic/list.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from telegrambot.bot_views.generic.base import TemplateCommandView
from telegrambot.bot_views.generic.message import TemplateCommandView
from django.core.exceptions import ImproperlyConfigured
from django.db.models.query import QuerySet
from django.utils import six
Expand Down
96 changes: 96 additions & 0 deletions telegrambot/bot_views/generic/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from telegrambot.bot_views.generic.responses import TemplateResponse, KeyboardResponse
from telegrambot.bot_views.generic.base import BaseCommandView
from telegram import ParseMode, ReplyKeyboardRemove
import sys
import logging

logger = logging.getLogger(__name__)
PY3 = sys.version_info > (3,)


class SendMessageCommandView(BaseCommandView):
message = None
keyboard = ReplyKeyboardRemove()
parse_mode = ParseMode.MARKDOWN

def __init__(self, **initkwargs):
if initkwargs.get('message', None) is not None:
self.message = initkwargs.get('message')

def get_chat_id(self):
return self.update.message.chat_id

def get_keyboard(self):
return self.keyboard

def get_message(self):
return self.message

def get_parse_mode(self):
return self.parse_mode

def handle(self):
self.send_message()

def send_message(self):
chat_id = self.get_chat_id()
text = self.get_message()
keyboard = self.get_keyboard()
parse_mode = self.get_parse_mode()

if not text:
logger.info('No text response for update %s' % str(self.update))
return

if not PY3:
text = text.encode('utf-8')

self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard, parse_mode=parse_mode)


class HelloWorldCommandView(SendMessageCommandView):
message = 'Hello World!'


class EchoCommandView(SendMessageCommandView):
"""Command which responds with the message received."""
def get_message(self):
return self.update.message.text


class TemplateCommandView(SendMessageCommandView):
"""Send a message from a template

Use the properties 'template_text' and 'template_keyboard' to define
which template to use for rendering the message and the keyboard.
And override the method 'get_context' to return the context variables
for both templates.
"""
template_text = None
template_keyboard = None

def get_context(self, bot, update, **kwargs):
return None

def get_keyboard(self):
ctx = self.get_context(self.bot, self.update, **self.kwargs)
return KeyboardResponse(self.template_keyboard, ctx).render()

def get_message(self):
ctx = self.get_context(self.bot, self.update, **self.kwargs)
text = TemplateResponse(self.template_text, ctx).render()
return text

def handle(self, *args, **kwargs):
# To maintain backwards compatibility we re-implement part of what is done in BaseCommandView::init so
# that the logic in this class can work fine even if the method init wasn't called as it should.
if len(args) > 0 or 'kwargs' in kwargs:
logger.warning("The arguments bot, update and kwargs should not be passed to handle(), "
" they are now accessible as properties. Support for this will be removed in the future. "
" Were you trying to trigger the view manually? In which case,"
" use View::as_command_view()(bot, update, **kwargs) instead.")
self._bot = args[0]
self._update = args[1]
self._kwargs = kwargs.get('kwargs', {})

super(TemplateCommandView, self).handle()
4 changes: 1 addition & 3 deletions telegrambot/bot_views/generic/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ def render(self):
return template.render(ctx)

class TextResponse(TemplateResponse):

def __init__(self, template_text, ctx=None):
super(TextResponse, self).__init__(template_text, ctx)
pass

class KeyboardResponse(TemplateResponse):

Expand Down
13 changes: 11 additions & 2 deletions telegrambot/test/testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _test_message_ok(self, action, update=None, number=1):
self.assertBotResponse(mock_send, action)
self.assertEqual(number, Update.objects.count())
self.assertUpdate(Update.objects.get(update_id=update.update_id), update)

def _test_message_no_handler(self, action, update=None, number=1):
if not update:
update = self.update
Expand All @@ -96,4 +96,13 @@ def _test_message_no_handler(self, action, update=None, number=1):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(0, mock_send.call_count)
self.assertEqual(number, Update.objects.count())
self.assertUpdate(Update.objects.get(update_id=update.update_id), update)
self.assertUpdate(Update.objects.get(update_id=update.update_id), update)

def _test_no_response(self, action, update=None):
if not update:
update = self.update
with mock.patch("telegram.bot.Bot.sendMessage", callable=mock.MagicMock()) as mock_send:
update.message.text = action['in']
response = self.client.post(self.webhook_url, update.to_json(), **self.kwargs)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(0, mock_send.call_count)
7 changes: 6 additions & 1 deletion tests/bot_handlers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from tests.commands_views import StartView, AuthorCommandView, AuthorInverseListView, AuthorCommandQueryView, \
UnknownView, AuthorName, MessageView
UnknownView, AuthorName, MessageView, MissingTemplateView
from telegrambot.handlers import command, unknown_command, regex, message
from telegrambot.bot_views.decorators import login_required
from telegrambot.bot_views.generic import SendMessageCommandView, EchoCommandView, HelloWorldCommandView

urlpatterns = [
command('start', StartView.as_command_view()),
Expand All @@ -10,6 +11,10 @@
regex(r'^author_(?P<name>\w+)', AuthorName.as_command_view()),
command('author_auth', login_required(AuthorCommandView.as_command_view())),
command('author', AuthorCommandView.as_command_view()),
command('hello', HelloWorldCommandView.as_command_view()),
command('how_are_you', SendMessageCommandView.as_command_view(message='Good, thanks!')),
command('missing', MissingTemplateView.as_command_view()),
regex(r'^Echo', EchoCommandView.as_command_view()),
unknown_command(UnknownView.as_command_view()),
message(MessageView.as_command_view())
]
3 changes: 3 additions & 0 deletions tests/commands_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ class AuthorName(DetailCommandView):

def get_slug(self, **kwargs):
return kwargs.get('name', None)

class MissingTemplateView(TemplateCommandView):
template_text = "i/dont/exist.txt"
Loading