Skip to content

Commit f355af1

Browse files
committed
Restoring new features after rebase
1 parent 121e365 commit f355af1

5 files changed

Lines changed: 406 additions & 0 deletions

File tree

telegram/command_handler.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from inspect import getmembers, ismethod
2+
import threading
3+
import logging
4+
import telegram
5+
import time
6+
logger = logging.getLogger(__name__)
7+
__all__ = ['CommandHandler', 'CommandHandlerWithHelp']
8+
class CommandHandler(object):
9+
""" This handles incomming commands and gives an easy way to create commands.
10+
11+
How to use this:
12+
create a new class which inherits this class or CommandHandlerWithHelp.
13+
define new methods that start with 'command_' and then the command_name.
14+
run run()
15+
"""
16+
def __init__(self, bot):
17+
self.bot = bot # a telegram bot
18+
self.isValidCommand = None # a function that returns a boolean and takes one agrument an update. if False is returned the the comaand is not executed.
19+
20+
def _get_command_func(self, command):
21+
if command[0] == '/':
22+
command = command[1:]
23+
if hasattr(self, 'command_' + command):
24+
return self.__getattribute__('command_' + command) # a function
25+
else:
26+
return None
27+
28+
def run(self, make_thread=True, last_update_id=None, thread_timeout=2, sleep=0.2):
29+
"""Continuously check for commands and run the according method
30+
31+
Args:
32+
make_thread:
33+
if True make a thread for each command it found.
34+
if False make run the code linearly
35+
last_update:
36+
the offset arg from getUpdates and is kept up to date within this function
37+
thread_timeout:
38+
The timeout on a thread. If a thread is alive after this period then try to join the thread in
39+
the next loop.
40+
"""
41+
old_threads = []
42+
while True:
43+
time.sleep(sleep)
44+
threads, last_update_id = self.run_once(make_thread=make_thread, last_update_id=last_update_id)
45+
for t in threads:
46+
t.start()
47+
for t in old_threads:
48+
threads.append(t)
49+
old_threads = []
50+
for t in threads:
51+
t.join(timeout=thread_timeout)
52+
if t.isAlive():
53+
old_threads.append(t)
54+
55+
def run_once(self, make_thread=True, last_update_id=None):
56+
""" Check the the messages for commands and make a Thread with the command or run the command depending on make_thread.
57+
58+
Args:
59+
make_thread:
60+
True: the function returns a list with threads. Which didn't start yet.
61+
False: the function just runs the command it found and returns an empty list.
62+
last_update_id:
63+
the offset arg from getUpdates and is kept up to date within this function
64+
65+
Returns:
66+
A tuple of two elements. The first element is a list with threads which didn't start yet or an empty list if
67+
make_threads==False. The second element is the updated las_update_id
68+
"""
69+
bot_name = self.bot.getMe().username
70+
threads = []
71+
try:
72+
updates = self.bot.getUpdates(offset=last_update_id)
73+
except:
74+
updates = []
75+
for update in updates:
76+
last_update_id = update.update_id + 1
77+
message = update.message
78+
if message.text[0] == '/':
79+
command, username = message.text.split(' ')[0], bot_name
80+
if '@' in command:
81+
command, username = command.split('@')
82+
if username == bot_name:
83+
command_func = self._get_command_func(command)
84+
if command_func is not None:
85+
self.bot.sendChatAction(update.message.chat.id,telegram.ChatAction.TYPING)
86+
if self.isValidCommand is None or self.isValidCommand(update):
87+
if make_thread:
88+
t = threading.Thread(target=command_func, args=(update,))
89+
threads.append(t)
90+
else:
91+
command_func(update)
92+
else:
93+
self._command_not_found(update) # TODO this must be another function.
94+
else:
95+
if make_thread:
96+
t = threading.Thread(target=self._command_not_found, args=(update,))
97+
threads.append(t)
98+
else:
99+
self._command_not_valid(update)
100+
return threads, last_update_id
101+
102+
def _command_not_valid(self, update):
103+
"""Inform the telegram user that the command was not found.
104+
105+
Override this method if you want to do it another way then by sending the the text:
106+
Sorry, I didn't understand the command: /command[@bot].
107+
"""
108+
chat_id = update.message.chat.id
109+
reply_to = update.message.message_id
110+
message = "Sorry, the command was not authorised or valid: {command}.".format(command=update.message.text.split(' ')[0])
111+
self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to)
112+
113+
def _command_not_found(self, update):
114+
"""Inform the telegram user that the command was not found.
115+
116+
Override this method if you want to do it another way then by sending the the text:
117+
Sorry, I didn't understand the command: /command[@bot].
118+
"""
119+
chat_id = update.message.chat.id
120+
reply_to = update.message.message_id
121+
message = "Sorry, I didn't understand the command: {command}.".format(command=update.message.text.split(' ')[0])
122+
self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to)
123+
124+
125+
class CommandHandlerWithHelp(CommandHandler):
126+
""" This CommandHandler has a builtin /help. It grabs the text from the docstrings of command_ functions."""
127+
def __init__(self, bot):
128+
super(CommandHandlerWithHelp, self).__init__(bot)
129+
self._help_title = 'Welcome to {name}.'.format(name=self.bot.getMe().username) # the title of help
130+
self._help_before_list = '' # text with information about the bot
131+
self._help_after_list = '' # a footer
132+
self._help_list_title = 'These are the commands:' # the title of the list
133+
self.is_reply = True
134+
self.command_start = self.command_help
135+
136+
def _generate_help(self):
137+
""" Generate a string which can be send as a help file.
138+
139+
This function generates a help file from all the docstrings from the commands.
140+
so docstrings of methods that start with command_ should explain what a command does and how a to use the
141+
command to the telegram user.
142+
"""
143+
144+
command_functions = [attr[1] for attr in getmembers(self, predicate=ismethod) if attr[0][:8] == 'command_']
145+
help_message = self._help_title + '\n\n'
146+
help_message += self._help_before_list + '\n\n'
147+
help_message += self._help_list_title + '\n'
148+
for command_function in command_functions:
149+
if command_function.__doc__ is not None:
150+
help_message += ' /' + command_function.__name__[8:] + ' - ' + command_function.__doc__ + '\n'
151+
else:
152+
help_message += ' /' + command_function.__name__[8:] + ' - ' + '\n'
153+
help_message += '\n'
154+
help_message += self._help_after_list
155+
return help_message
156+
157+
def _command_not_found(self, update):
158+
"""Inform the telegram user that the command was not found."""
159+
chat_id = update.message.chat.id
160+
reply_to = update.message.message_id
161+
message = 'Sorry, I did not understand the command: {command}. Please see /help for all available commands'
162+
if self.is_reply:
163+
self.bot.sendMessage(chat_id, message.format(command=update.message.text.split(' ')[0]),
164+
reply_to_message_id=reply_to)
165+
else:
166+
self.bot.sendMessage(chat_id, message.format(command=update.message.text.split(' ')[0]))
167+
168+
def command_help(self, update):
169+
""" The help file. """
170+
chat_id = update.message.chat.id
171+
reply_to = update.message.message_id
172+
message = self._generate_help()
173+
self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to)
174+

telegram/enchancedbot.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import telegram
2+
3+
4+
class NoSuchCommandException(BaseException):
5+
pass
6+
7+
class CommandDispatcher:
8+
def __init__(self,):
9+
self.commands = list()
10+
self.default = None
11+
12+
def addCommand(self, command, callback):
13+
self.commands.append((command, callback))
14+
15+
def setDefault(self, callback):
16+
self.default = callback
17+
18+
def dispatch(self, update):
19+
if hasattr(update.message, 'text'):
20+
text = update.message.text
21+
else:
22+
text = ''
23+
24+
user_id = update.message.from_user.id
25+
com = text.split('@')[0]
26+
for command, callback in self.commands:
27+
if com == command:
28+
return callback(command, user_id)
29+
if self.default is not None:
30+
return self.default(text, user_id)
31+
else:
32+
raise NoSuchCommandException()
33+
34+
35+
class EnhancedBot(telegram.Bot):
36+
"""The Bot class with command dispatcher added.
37+
38+
>>> bot = EnhancedBot(token=TOKEN)
39+
>>> @bot.command('/start')
40+
... def start(command, user_id):
41+
... # should return a tuple: (text, reply_id, custom_keyboard)
42+
... return ("Hello, there! Your id is {}".format(user_id), None, None)
43+
>>> while True:
44+
... bot.processUpdates()
45+
... time.sleep(3)
46+
"""
47+
def __init__(self, token):
48+
self.dispatcher = CommandDispatcher()
49+
telegram.Bot.__init__(self, token=token)
50+
self.offset = 0 #id of the last processed update
51+
52+
def command(self, *names, default=False):
53+
"""Decorator for adding callbacks for commands."""
54+
55+
def inner_command(callback):
56+
for name in names:
57+
self.dispatcher.addCommand(name, callback)
58+
if default:
59+
self.dispatcher.setDefault(callback)
60+
return callback # doesn't touch the callback, so we can use it
61+
return inner_command
62+
63+
def processUpdates(self):
64+
updates = self.getUpdates(offset=self.offset)
65+
66+
for update in updates:
67+
print('processing update: {}'.format(str(update.to_dict())))
68+
self.offset = update.update_id + 1
69+
if not hasattr(update, 'message'):
70+
continue
71+
72+
try:
73+
answer, reply_to, reply_markup = self.dispatcher.dispatch(update)
74+
except Exception as e:
75+
print('error occured') # TODO logging
76+
print(update.to_dict())
77+
raise e
78+
79+
if answer is not None:
80+
self.sendMessage(chat_id=update.message.chat_id,
81+
text=answer,
82+
reply_to_message_id=reply_to,
83+
reply_markup=reply_markup)

telegram/utils/__init__.py

Whitespace-only changes.

telegram/utils/botan.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python
2+
#
3+
# A library that provides a Python interface to the Telegram Bot API
4+
# Copyright (C) 2015 Leandro Toledo de Souza <leandrotoeldodesouza@gmail.com>
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser Public License
17+
# along with this program. If not, see [http://www.gnu.org/licenses/].
18+
19+
import json
20+
try:
21+
from urllib.parse import urlencode
22+
from urllib.request import urlopen, Request
23+
from urllib.error import HTTPError, URLError
24+
except ImportError:
25+
from urllib import urlencode
26+
from urllib2 import urlopen, Request
27+
from urllib2 import HTTPError, URLError
28+
29+
DEFAULT_BASE_URL = \
30+
'https://api.botan.io/track?token=%(token)&uid=%(uid)&name=%(name)'
31+
USER_AGENT = 'Python Telegram Bot' \
32+
' (https://github.com/leandrotoledo/python-telegram-bot)'
33+
CONTENT_TYPE = 'application/json'
34+
35+
class Botan(Object):
36+
def __init__(self,
37+
token,
38+
base_url=None):
39+
self.token = token
40+
41+
if base_url is None:
42+
self.base_url = DEFAULT_BASE_URL % {'token': self.token}
43+
else:
44+
self.base_url = base_url % {'token': self.token}
45+
46+
def track(self,
47+
uid,
48+
text,
49+
name = 'Message'):
50+
51+
url = self.base_url % {'uid': uid,
52+
'message': text,
53+
'name': name}
54+
55+
self._post(url, message)
56+
57+
def _post(self,
58+
url,
59+
data):
60+
headers = {'User-agent': USER_AGENT,
61+
'Content-type': CONTENT_TYPE}
62+
63+
try:
64+
request = Request(
65+
url,
66+
data=urlencode(data).encode(),
67+
headers=headers
68+
)
69+
70+
return urlopen(request).read()
71+
except IOError as e:
72+
raise TelegramError(str(e))
73+
except HTTPError as e:
74+
raise TelegramError(str(e))

0 commit comments

Comments
 (0)