2017-02-25 20:49:06 +00:00
|
|
|
import asyncio
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
import aiohttp
|
2017-02-26 18:52:44 +00:00
|
|
|
import datetime
|
2017-03-05 17:14:45 +00:00
|
|
|
|
2017-02-25 20:49:06 +00:00
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
class TelegramAPIError(Exception):
|
2017-02-25 20:49:06 +00:00
|
|
|
pass
|
|
|
|
|
2017-03-05 17:14:45 +00:00
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
class UpdateError(Exception):
|
|
|
|
pass
|
2017-02-25 20:49:06 +00:00
|
|
|
|
2017-03-05 17:14:45 +00:00
|
|
|
|
2017-02-25 20:49:06 +00:00
|
|
|
class Bot:
|
|
|
|
def __init__(self, token):
|
|
|
|
self.token = token
|
|
|
|
self.user_data = None
|
|
|
|
self.updates = list()
|
|
|
|
self.chats = list()
|
2017-02-27 09:46:52 +00:00
|
|
|
self.commands = dict()
|
2017-02-26 18:52:44 +00:00
|
|
|
self.offset = 0
|
2017-02-25 20:49:06 +00:00
|
|
|
# Update user_data
|
2017-02-27 09:46:52 +00:00
|
|
|
loop.create_task(self.update_bot_data())
|
2017-02-25 20:49:06 +00:00
|
|
|
|
2017-03-06 07:36:04 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.user_data.first_name
|
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<Bot {self.user_data.first_name}>"
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.token)
|
|
|
|
|
2017-03-18 20:32:34 +00:00
|
|
|
async def run(self):
|
2017-02-27 22:03:38 +00:00
|
|
|
"""Run the bot automatically."""
|
|
|
|
while True:
|
2017-03-18 20:32:34 +00:00
|
|
|
await self.get_updates()
|
2017-02-27 22:03:38 +00:00
|
|
|
for u in self.updates:
|
|
|
|
loop.create_task(self.parse_update(u))
|
|
|
|
self.updates = list()
|
2017-03-22 17:01:52 +00:00
|
|
|
# Wait 5 seconds between two requests, allowing the parsing of updates.
|
|
|
|
await asyncio.sleep(5)
|
2017-02-27 22:03:38 +00:00
|
|
|
|
2017-02-25 20:49:06 +00:00
|
|
|
async def update_bot_data(self):
|
|
|
|
"""Update self.user_data with the latest information from /getMe."""
|
|
|
|
data = await self.api_request("getMe")
|
|
|
|
self.user_data = User(data)
|
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
async def get_updates(self):
|
|
|
|
"""Get the latest updates from the Telegram API with /getUpdates."""
|
2017-02-26 19:19:54 +00:00
|
|
|
try:
|
2017-03-22 17:01:52 +00:00
|
|
|
# TODO: Fix long polling
|
|
|
|
data = await self.api_request("getUpdates", offset=self.offset)
|
2017-02-26 19:19:54 +00:00
|
|
|
except asyncio.TimeoutError:
|
|
|
|
return
|
2017-02-26 18:52:44 +00:00
|
|
|
for update in data:
|
|
|
|
try:
|
|
|
|
self.updates.append(Update(update))
|
|
|
|
except NotImplementedError:
|
|
|
|
pass
|
|
|
|
if len(data) > 0:
|
|
|
|
self.offset = self.updates[-1].update_id + 1
|
|
|
|
|
2017-02-27 10:26:41 +00:00
|
|
|
async def parse_update(self, update):
|
2017-02-26 18:52:44 +00:00
|
|
|
"""Parse the first update in the list."""
|
2017-02-27 10:26:41 +00:00
|
|
|
# Add the chat to the chat list
|
2017-02-26 18:52:44 +00:00
|
|
|
if update.message.chat not in self.chats:
|
|
|
|
self.chats.append(update.message.chat)
|
2017-02-27 22:52:47 +00:00
|
|
|
else:
|
|
|
|
# Replace the chat object in the update with the correct one
|
|
|
|
update.message.chat = self.chats[self.chats.index(update.message.chat)]
|
2017-02-27 10:26:41 +00:00
|
|
|
# Add the user to the chat
|
2017-02-26 18:52:44 +00:00
|
|
|
chat = self.find_chat(update.message.chat.chat_id)
|
|
|
|
if update.message.sent_from not in chat.users:
|
|
|
|
chat.users.append(update.message.sent_from)
|
2017-02-27 22:52:47 +00:00
|
|
|
else:
|
|
|
|
update.message.sent_from = chat.users[chat.users.index(update.message.sent_from)]
|
2017-02-27 10:26:41 +00:00
|
|
|
# Add / edit the message to the message list
|
2017-02-26 19:19:54 +00:00
|
|
|
if not update.message.edited:
|
|
|
|
chat.messages.append(update.message)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
i = chat.messages.index(chat.find_message(update.message.msg_id))
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
chat.messages[i] = update.message
|
2017-02-27 10:26:41 +00:00
|
|
|
# Check if a command can be run
|
|
|
|
# TODO: use message entities?
|
|
|
|
if isinstance(update.message.content, str) and update.message.content.startswith("/"):
|
2017-02-27 22:52:47 +00:00
|
|
|
split_msg = update.message.content.split(" ")
|
2017-03-05 17:14:45 +00:00
|
|
|
# Ignore the left slash and the right @botname
|
|
|
|
command = split_msg[0].lstrip("/").split("@")[0]
|
2017-02-27 10:26:41 +00:00
|
|
|
if command in self.commands:
|
2017-02-27 22:52:47 +00:00
|
|
|
arguments = split_msg[1:]
|
|
|
|
loop.create_task(self.commands[command](bot=self, update=update, arguments=arguments))
|
2017-02-27 11:55:30 +00:00
|
|
|
# Update message status if a service message is received
|
|
|
|
if isinstance(update.message.content, ServiceMessage):
|
|
|
|
# New user in chat
|
|
|
|
if update.message.content.type == "new_chat_user":
|
|
|
|
new_user = update.message.content.content
|
|
|
|
chat.users.append(new_user)
|
|
|
|
# User left chat
|
|
|
|
elif update.message.content.type == "left_chat_user":
|
|
|
|
left_user = update.message.content.content
|
|
|
|
if left_user in chat.users:
|
|
|
|
# Remove the user from the list
|
|
|
|
del chat.users[chat.users.index(left_user)]
|
|
|
|
# Chat title changed
|
|
|
|
elif update.message.content.type == "new_chat_title":
|
|
|
|
chat.title = update.message.content.content
|
|
|
|
# New chat photo
|
|
|
|
elif update.message.content.type == "new_chat_photo":
|
|
|
|
chat.chat_photo = update.message.content.content
|
|
|
|
# Chat photo deleted
|
|
|
|
elif update.message.content.type == "delete_chat_photo":
|
|
|
|
chat.chat_photo = None
|
|
|
|
# New pinned message
|
|
|
|
elif update.message.content.type == "pinned_message":
|
|
|
|
chat.pinned_msg = update.message.content.content
|
2017-03-05 17:14:45 +00:00
|
|
|
# TODO: handle group -> supergroup migrations
|
2017-02-27 10:26:41 +00:00
|
|
|
|
|
|
|
def find_update(self, upd_id):
|
|
|
|
for update in self.updates:
|
|
|
|
if update.update_id == upd_id:
|
|
|
|
return update
|
2017-02-26 18:52:44 +00:00
|
|
|
|
|
|
|
def find_chat(self, chat_id):
|
|
|
|
for chat in self.chats:
|
|
|
|
if chat.chat_id == chat_id:
|
|
|
|
return chat
|
|
|
|
|
2017-03-12 17:50:18 +00:00
|
|
|
async def api_request(self, endpoint, **params):
|
2017-02-25 20:49:06 +00:00
|
|
|
"""Send a request to the Telegram API at the specified endpoint."""
|
2017-03-12 18:12:14 +00:00
|
|
|
# TODO: Reintroduce the timeout to prevent stuck requests
|
|
|
|
# Create a new session for each request.
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
# Send the request to the Telegram API
|
|
|
|
token = self.token
|
|
|
|
async with session.request("GET", f"https://api.telegram.org/bot{token}/{endpoint}", params=params) as response:
|
|
|
|
# Parse the json data as soon it's ready
|
|
|
|
data = await response.json()
|
2017-03-26 11:26:01 +00:00
|
|
|
# Check for errors in the request
|
|
|
|
if "description" in data:
|
|
|
|
error = data["description"]
|
|
|
|
if response.status != 200:
|
2017-03-26 11:26:49 +00:00
|
|
|
raise TelegramAPIError(f"Request returned {response.status} {response.reason}")
|
2017-03-22 18:13:11 +00:00
|
|
|
# Check for errors in the response
|
|
|
|
if not data["ok"]:
|
|
|
|
error = data["description"]
|
|
|
|
raise TelegramAPIError(f"Response returned an error: {error}")
|
|
|
|
# Return a dictionary containing the data
|
|
|
|
return data["result"]
|
2017-02-25 20:49:06 +00:00
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
|
|
|
|
class Update:
|
|
|
|
def __init__(self, upd_dict):
|
|
|
|
self.update_id = upd_dict["update_id"]
|
|
|
|
if "message" in upd_dict:
|
|
|
|
self.message = Message(upd_dict["message"])
|
|
|
|
elif "edited_message" in upd_dict:
|
2017-02-26 19:19:54 +00:00
|
|
|
self.message = Message(upd_dict["edited_message"], edited=True)
|
2017-02-26 18:52:44 +00:00
|
|
|
elif "channel_post" in upd_dict:
|
|
|
|
self.message = Message(upd_dict["channel_post"])
|
|
|
|
elif "edited_channel_post" in upd_dict:
|
2017-02-26 19:19:54 +00:00
|
|
|
self.message = Message(upd_dict["edited_channel_post"], edited=True)
|
2017-02-26 18:52:44 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError("No inline support yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Chat:
|
|
|
|
def __init__(self, chat_dict):
|
|
|
|
self.chat_id = chat_dict["id"]
|
|
|
|
self.type = chat_dict["type"]
|
|
|
|
self.users = list()
|
|
|
|
self.admins = list()
|
|
|
|
self.messages = list()
|
|
|
|
self.chat_photo = None
|
2017-02-27 11:55:30 +00:00
|
|
|
self.pinned_msg = None
|
2017-02-26 18:52:44 +00:00
|
|
|
if self.type == "private":
|
|
|
|
self.first_name = chat_dict["first_name"]
|
|
|
|
if "last_name" in chat_dict:
|
|
|
|
self.last_name = chat_dict["last_name"]
|
|
|
|
else:
|
|
|
|
self.last_name = None
|
|
|
|
if "username" in chat_dict:
|
|
|
|
self.username = chat_dict["username"]
|
|
|
|
else:
|
|
|
|
self.username = None
|
|
|
|
self.title = f"{self.first_name} {self.last_name}"
|
|
|
|
self.everyone_is_admin = True
|
|
|
|
elif self.type == "group" or self.type == "supergroup" or self.type == "channel":
|
|
|
|
self.first_name = None
|
|
|
|
self.last_name = None
|
|
|
|
if self.type == "supergroup" or self.type == "channel":
|
|
|
|
self.everyone_is_admin = False
|
|
|
|
if "username" in chat_dict:
|
|
|
|
self.username = chat_dict["username"]
|
|
|
|
else:
|
|
|
|
self.username = None
|
|
|
|
else:
|
|
|
|
self.everyone_is_admin = chat_dict["all_members_are_administrators"]
|
|
|
|
self.username = None
|
|
|
|
self.title = chat_dict["title"]
|
|
|
|
else:
|
|
|
|
raise UpdateError(f"Unknown message type: {self.type}")
|
|
|
|
|
2017-03-06 07:36:04 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<{self.type} Chat {self.title}>"
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return self.chat_id
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, Chat):
|
|
|
|
return self.chat_id == other.chat_id
|
|
|
|
else:
|
|
|
|
TypeError("Can't compare Chat to a different object.")
|
|
|
|
|
2017-02-26 19:19:54 +00:00
|
|
|
def find_message(self, msg_id):
|
|
|
|
for msg in self.messages:
|
|
|
|
if msg.msg_id == msg_id:
|
|
|
|
return msg
|
|
|
|
|
2017-02-27 22:52:47 +00:00
|
|
|
async def send_message(self, bot, text, **params):
|
|
|
|
"""Send a message in the chat through the bot object."""
|
2017-03-22 16:50:49 +00:00
|
|
|
# TODO: This could give problems if a class inherits Bot
|
|
|
|
if not isinstance(bot, Bot):
|
|
|
|
raise TypeError("bot is not an instance of Bot.")
|
2017-03-27 10:45:22 +00:00
|
|
|
await bot.api_request("sendMessage", text=text, chat_id=self.chat_id, **params)
|
2017-02-26 19:19:54 +00:00
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
|
2017-03-27 10:48:41 +00:00
|
|
|
async def set_chat_action(self, bot, action):
|
|
|
|
"""Set a status for the chat.
|
|
|
|
|
|
|
|
Valid actions are:
|
|
|
|
typing
|
|
|
|
upload_photo
|
|
|
|
record_video
|
|
|
|
upload_video
|
|
|
|
record_audio
|
|
|
|
upload_audio
|
|
|
|
upload_document
|
|
|
|
find_location"""
|
|
|
|
# TODO: This could give problems if a class inherits Bot
|
|
|
|
if not isinstance(bot, Bot):
|
|
|
|
raise TypeError("bot is not an instance of Bot.")
|
|
|
|
# Check if the action is valid
|
|
|
|
if action not in ["typing", "upload_photo", "record_video", "upload_video", "record_audio", "upload_audio", "upload_document", "find_location"]:
|
|
|
|
raise ValueError("Invalid action")
|
|
|
|
# Send the request
|
|
|
|
await bot.api_request("sendChatAction", chat_id=self.chat_id, action=action)
|
|
|
|
|
|
|
|
|
2017-02-25 20:49:06 +00:00
|
|
|
class User:
|
|
|
|
def __init__(self, user_dict):
|
|
|
|
self.user_id = user_dict["id"]
|
|
|
|
self.first_name = user_dict["first_name"]
|
|
|
|
if "last_name" in user_dict:
|
|
|
|
self.last_name = user_dict["last_name"]
|
|
|
|
else:
|
|
|
|
self.last_name = None
|
|
|
|
if "username" in user_dict:
|
|
|
|
self.username = user_dict["username"]
|
|
|
|
else:
|
|
|
|
self.username = None
|
2017-02-26 18:52:44 +00:00
|
|
|
|
2017-03-05 18:12:02 +00:00
|
|
|
def __str__(self):
|
|
|
|
if self.username is not None:
|
|
|
|
return f"@{self.username}"
|
|
|
|
else:
|
|
|
|
if self.last_name is not None:
|
|
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
else:
|
|
|
|
return self.first_name
|
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
def __repr__(self):
|
|
|
|
if self.username is not None:
|
|
|
|
return f"<User {self.username}>"
|
|
|
|
else:
|
|
|
|
return f"<User {self.user_id}>"
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return self.user_id
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, User):
|
|
|
|
return self.user_id == other.user_id
|
|
|
|
else:
|
|
|
|
TypeError("Can't compare User to a different object.")
|
|
|
|
|
|
|
|
|
|
|
|
class Message:
|
2017-02-26 19:19:54 +00:00
|
|
|
def __init__(self, msg_dict, edited=False):
|
2017-02-26 18:52:44 +00:00
|
|
|
self.msg_id = msg_dict["message_id"]
|
|
|
|
self.date = datetime.datetime.fromtimestamp(msg_dict["date"])
|
|
|
|
self.chat = Chat(msg_dict["chat"])
|
2017-02-26 19:19:54 +00:00
|
|
|
self.edited = edited
|
2017-02-26 18:52:44 +00:00
|
|
|
if "from" in msg_dict:
|
|
|
|
self.sent_from = User(msg_dict["from"])
|
|
|
|
else:
|
|
|
|
self.sent_from = None
|
|
|
|
self.forwarded = "forward_date" in msg_dict
|
|
|
|
if self.forwarded:
|
|
|
|
if "forward_from" in msg_dict:
|
|
|
|
self.original_sender = User(msg_dict["forward_from"])
|
|
|
|
elif "forward_from_chat" in msg_dict:
|
|
|
|
self.original_sender = Chat(msg_dict["forward_from_chat"])
|
|
|
|
# TODO: Add forward_from_message_id
|
|
|
|
if "reply_to_message" in msg_dict:
|
|
|
|
self.is_reply_to = Message(msg_dict["reply_to_message"])
|
|
|
|
else:
|
|
|
|
self.is_reply_to = None
|
|
|
|
if "text" in msg_dict:
|
|
|
|
self.content = msg_dict["text"]
|
|
|
|
# TODO: Check for MessageEntities
|
|
|
|
elif "audio" in msg_dict:
|
|
|
|
self.content = Audio(msg_dict["audio"])
|
|
|
|
elif "document" in msg_dict:
|
|
|
|
self.content = Document(msg_dict["document"])
|
|
|
|
elif "game" in msg_dict:
|
|
|
|
self.content = Game(msg_dict["game"])
|
|
|
|
elif "photo" in msg_dict:
|
|
|
|
self.content = Photo(msg_dict["photo"])
|
|
|
|
elif "sticker" in msg_dict:
|
|
|
|
self.content = Sticker(msg_dict["sticker"])
|
|
|
|
elif "video" in msg_dict:
|
|
|
|
self.content = Video(msg_dict["video"])
|
|
|
|
elif "voice" in msg_dict:
|
|
|
|
self.content = Voice(msg_dict["voice"])
|
|
|
|
elif "contact" in msg_dict:
|
|
|
|
self.content = Contact(msg_dict["contact"])
|
|
|
|
elif "location" in msg_dict:
|
|
|
|
self.content = Location(msg_dict["location"])
|
|
|
|
elif "venue" in msg_dict:
|
|
|
|
self.content = Venue(msg_dict["venue"])
|
|
|
|
elif "new_chat_member" in msg_dict:
|
|
|
|
self.content = ServiceMessage("new_chat_member", User(msg_dict["new_chat_member"]))
|
|
|
|
elif "left_chat_member" in msg_dict:
|
|
|
|
self.content = ServiceMessage("left_chat_member", User(msg_dict["left_chat_member"]))
|
|
|
|
elif "new_chat_title" in msg_dict:
|
|
|
|
self.content = ServiceMessage("new_chat_title", msg_dict["new_chat_title"])
|
|
|
|
elif "new_chat_photo" in msg_dict:
|
|
|
|
self.content = ServiceMessage("new_chat_photo", Photo(msg_dict["new_chat_photo"]))
|
|
|
|
elif "delete_chat_photo" in msg_dict:
|
|
|
|
self.content = ServiceMessage("delete_chat_photo")
|
|
|
|
elif "group_chat_created" in msg_dict:
|
|
|
|
self.content = ServiceMessage("group_chat_created")
|
|
|
|
elif "supergroup_chat_created" in msg_dict:
|
|
|
|
self.content = ServiceMessage("supergroup_chat_created")
|
|
|
|
elif "channel_chat_created" in msg_dict:
|
|
|
|
self.content = ServiceMessage("channel_chat_created")
|
|
|
|
elif "migrate_to_chat_id" in msg_dict:
|
|
|
|
self.content = ServiceMessage("migrate_to_chat_id", msg_dict["migrate_to_chat_id"])
|
|
|
|
elif "migrate_from_chat_id" in msg_dict:
|
|
|
|
self.content = ServiceMessage("migrate_from_chat_id", msg_dict["migrate_from_chat_id"])
|
|
|
|
elif "pinned_message" in msg_dict:
|
|
|
|
self.content = ServiceMessage("pinned_message", Message(msg_dict["pinned_message"]))
|
|
|
|
else:
|
|
|
|
raise UpdateError("Message doesn't contain anything.")
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
if isinstance(self.content, str):
|
|
|
|
return f"<Message: {self.content}>"
|
|
|
|
else:
|
|
|
|
return f"<Message containing {type(self.content)}>"
|
|
|
|
|
2017-03-09 11:52:06 +00:00
|
|
|
async def reply(self, bot, text, **params):
|
2017-03-06 07:36:04 +00:00
|
|
|
"""Reply to this message."""
|
2017-03-09 11:52:06 +00:00
|
|
|
await self.chat.send_message(bot, text, reply_to_message_id=self.msg_id, **params)
|
2017-02-26 18:52:44 +00:00
|
|
|
|
2017-03-10 10:59:00 +00:00
|
|
|
|
2017-02-26 18:52:44 +00:00
|
|
|
class ServiceMessage:
|
|
|
|
def __init__(self, msg_type, extra=None):
|
|
|
|
self.type = msg_type
|
|
|
|
self.content = extra
|
|
|
|
|
|
|
|
|
|
|
|
class Audio:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Document:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Game:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Photo:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Sticker:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Video:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Voice:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Contact:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Location:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|
|
|
|
|
|
|
|
|
|
|
|
class Venue:
|
|
|
|
def __init__(self, init_dict):
|
|
|
|
raise NotImplementedError("Not yet.")
|