-
Notifications
You must be signed in to change notification settings - Fork 52
Telegram v2 #168
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
Merged
Merged
Telegram v2 #168
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
12cc75c
Minor changes
leddcode f5f29c0
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode a7793c3
Add telegram client
leddcode bfe2ceb
Apply merge changes
leddcode 7af4147
Minor fixes
leddcode 8433c4a
Minor fixes
leddcode 091db8e
Create FastAPI Settings object
leddcode 8e93a36
Use status code names in telegram tests
leddcode b8d0cba
Use status code names in telegram tests
leddcode 198e4d6
Add Asynchronous Testing and Increase Code Coverage.
leddcode 3fc9d49
Added keyboards, handlers and tests. Updated telegram models.
leddcode 798a72f
Update tests.
leddcode 3078682
Minor fixes.
leddcode 94a8c01
Add test bot client with registered user
leddcode b0bc592
Minor fix
leddcode de2995c
Make telegram bot working async. Add async fixture and tests.
leddcode e618089
Resolve conflict on merge
leddcode 2122222
Add telegram tests
leddcode c1d309a
Make request.post works asynchronous
leddcode 8795a7b
Merge updates
leddcode a5b4a6f
Minor change
leddcode 08e65de
Merge updates to telegram
leddcode 6229fc0
Add an ability to create events on telegram
leddcode f86f5a1
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode 245c0c0
Merge updates from develop
leddcode 316c3ab
Test creating new events
leddcode 5dcc8ac
Minor fix
leddcode 2bc356a
Merge changes
leddcode 68d75d2
Splitting to functions
leddcode a9d8205
Merge updates
leddcode 28fe8e8
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode 2ff5de3
Minor changes
leddcode 2a20efc
Merge changes
leddcode 2086e56
Set and drop webhook asynchronously
leddcode 73cb88c
Merge requirements.txt
leddcode 1137770
Merge requirements.txt
leddcode 498ef67
Minor fix
leddcode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,16 @@ | ||
import datetime | ||
|
||
import asyncio | ||
import dateparser | ||
|
||
from .keyboards import ( | ||
DATE_FORMAT, gen_inline_keyboard, get_this_week_buttons, show_events_kb) | ||
DATE_FORMAT, field_kb, gen_inline_keyboard, | ||
get_this_week_buttons, new_event_kb, show_events_kb) | ||
from .models import Chat | ||
from .bot import telegram_bot | ||
from app.database.database import get_db | ||
from app.database.models import User | ||
from app.routers.event import create_event | ||
|
||
|
||
class MessageHandler: | ||
|
@@ -14,6 +20,7 @@ def __init__(self, chat: Chat, user: User): | |
self.handlers = {} | ||
self.handlers['/start'] = self.start_handler | ||
self.handlers['/show_events'] = self.show_events_handler | ||
self.handlers['/new_event'] = self.new_event_handler | ||
self.handlers['Today'] = self.today_handler | ||
self.handlers['This week'] = self.this_week_handler | ||
|
||
|
@@ -23,7 +30,10 @@ def __init__(self, chat: Chat, user: User): | |
self.handlers[button['text']] = self.chosen_day_handler | ||
|
||
async def process_callback(self): | ||
if self.chat.message in self.handlers: | ||
if self.chat.user_id in telegram_bot.MEMORY: | ||
return await self.process_new_event( | ||
telegram_bot.MEMORY[self.chat.user_id]) | ||
elif self.chat.message in self.handlers: | ||
return await self.handlers[self.chat.message]() | ||
return await self.default_handler() | ||
|
||
|
@@ -34,7 +44,7 @@ async def default_handler(self): | |
|
||
async def start_handler(self): | ||
answer = f'''Hello, {self.chat.first_name}! | ||
Welcome to Pylander telegram client!''' | ||
Welcome to PyLendar telegram client!''' | ||
await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) | ||
return answer | ||
|
||
|
@@ -52,17 +62,20 @@ async def today_handler(self): | |
_.events for _ in self.user.events | ||
if _.events.start <= today <= _.events.end] | ||
|
||
answer = f"{today.strftime('%B %d')}, {today.strftime('%A')} Events:\n" | ||
|
||
if not events: | ||
answer = "There're no events today." | ||
return await self._process_no_events_today() | ||
|
||
answer = f"{today.strftime('%A, %B %d')}:\n" | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
for event in events: | ||
answer += f''' | ||
From {event.start.strftime('%d/%m %H:%M')} \ | ||
to {event.end.strftime('%d/%m %H:%M')}: {event.title}.\n''' | ||
await self._send_event(event) | ||
return answer | ||
|
||
await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) | ||
async def _process_no_events_today(self): | ||
answer = "There're no events today." | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
return answer | ||
|
||
async def this_week_handler(self): | ||
|
@@ -76,34 +89,176 @@ async def this_week_handler(self): | |
return answer | ||
|
||
async def chosen_day_handler(self): | ||
# Convert chosen day (string) to datetime format | ||
chosen_date = datetime.datetime.strptime( | ||
self.chat.message, DATE_FORMAT) | ||
|
||
events = [ | ||
_.events for _ in self.user.events | ||
if _.events.start <= chosen_date <= _.events.end] | ||
|
||
answer = f"{chosen_date.strftime('%B %d')}, \ | ||
{chosen_date.strftime('%A')} Events:\n" | ||
|
||
if not events: | ||
answer = f"There're no events on {chosen_date.strftime('%B %d')}." | ||
return await self._process_no_events_on_date(chosen_date) | ||
|
||
answer = f"{chosen_date.strftime('%A, %B %d')}:\n" | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
for event in events: | ||
answer += f''' | ||
From {event.start.strftime('%d/%m %H:%M')} \ | ||
to {event.end.strftime('%d/%m %H:%M')}: {event.title}.\n''' | ||
await self._send_event(event) | ||
return answer | ||
|
||
await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) | ||
async def _process_no_events_on_date(self, date): | ||
answer = f"There're no events on {date.strftime('%B %d')}." | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
return answer | ||
|
||
async def _send_event(self, event): | ||
start = event.start.strftime("%d %b %Y %H:%M") | ||
end = event.end.strftime("%d %b %Y %H:%M") | ||
text = f'Title:\n{event.title}\n\n' | ||
text += f'Content:\n{event.content}\n\n' | ||
text += f'Location:\n{event.location}\n\n' | ||
text += f'Starts on:\n{start}\n\n' | ||
text += f'Ends on:\n{end}' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=text) | ||
await asyncio.sleep(1) | ||
|
||
async def process_new_event(self, memo_dict): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split to functions |
||
if self.chat.message == 'cancel': | ||
return await self._cancel_new_event_processing() | ||
elif self.chat.message == 'restart': | ||
return await self._restart_new_event_processing() | ||
elif 'title' not in memo_dict: | ||
return await self._process_title(memo_dict) | ||
elif 'content' not in memo_dict: | ||
return await self._process_content(memo_dict) | ||
elif 'location' not in memo_dict: | ||
return await self._process_location(memo_dict) | ||
elif 'start' not in memo_dict: | ||
return await self._process_start_date(memo_dict) | ||
elif 'end' not in memo_dict: | ||
return await self._process_end_date(memo_dict) | ||
elif self.chat.message == 'create': | ||
return await self._submit_new_event(memo_dict) | ||
|
||
async def new_event_handler(self): | ||
telegram_bot.MEMORY[self.chat.user_id] = {} | ||
answer = 'Please, give your event a title.' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _cancel_new_event_processing(self): | ||
del telegram_bot.MEMORY[self.chat.user_id] | ||
answer = '🚫 The process was canceled.' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
return answer | ||
|
||
async def _restart_new_event_processing(self): | ||
answer = await self.new_event_handler() | ||
return answer | ||
|
||
async def _process_title(self, memo_dict): | ||
memo_dict['title'] = self.chat.message | ||
answer = f'Title:\n{memo_dict["title"]}\n\n' | ||
answer += 'Add a description of the event.' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _process_content(self, memo_dict): | ||
memo_dict['content'] = self.chat.message | ||
answer = f'Content:\n{memo_dict["content"]}\n\n' | ||
answer += 'Where the event will be held?' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _process_location(self, memo_dict): | ||
memo_dict['location'] = self.chat.message | ||
answer = f'Location:\n{memo_dict["location"]}\n\n' | ||
answer += 'When does it start?' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _process_start_date(self, memo_dict): | ||
date = dateparser.parse(self.chat.message) | ||
if date: | ||
return await self._add_start_date(memo_dict, date) | ||
return await self._process_bad_date_input() | ||
|
||
async def _add_start_date(self, memo_dict, date): | ||
memo_dict['start'] = date | ||
answer = f'Starts on:\n{date.strftime("%d %b %Y %H:%M")}\n\n' | ||
answer += 'And when does it end?' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _process_bad_date_input(self): | ||
answer = '❗️ Please, enter a valid date/time.' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=field_kb) | ||
return answer | ||
|
||
async def _process_end_date(self, memo_dict): | ||
date = dateparser.parse(self.chat.message) | ||
if date: | ||
return await self._add_end_date(memo_dict, date) | ||
return await self._process_bad_date_input() | ||
|
||
async def _add_end_date(self, memo_dict, date): | ||
memo_dict['end'] = date | ||
start_time = memo_dict["start"].strftime("%d %b %Y %H:%M") | ||
answer = f'Title:\n{memo_dict["title"]}\n\n' | ||
answer += f'Content:\n{memo_dict["content"]}\n\n' | ||
answer += f'Location:\n{memo_dict["location"]}\n\n' | ||
answer += f'Starts on:\n{start_time}\n\n' | ||
answer += f'Ends on:\n{date.strftime("%d %b %Y %H:%M")}' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, | ||
text=answer, | ||
reply_markup=new_event_kb) | ||
return answer | ||
|
||
async def _submit_new_event(self, memo_dict): | ||
answer = 'New event was successfully created 🎉' | ||
await telegram_bot.send_message( | ||
chat_id=self.chat.user_id, text=answer) | ||
# Save to database | ||
create_event( | ||
db=next(get_db()), | ||
title=memo_dict['title'], | ||
start=memo_dict['start'], | ||
end=memo_dict['end'], | ||
content=memo_dict['content'], | ||
owner_id=self.user.id, | ||
location=memo_dict['location'], | ||
) | ||
# Delete current session | ||
del telegram_bot.MEMORY[self.chat.user_id] | ||
return answer | ||
|
||
|
||
async def reply_unknown_user(chat): | ||
answer = f''' | ||
Hello, {chat.first_name}! | ||
|
||
To use PyLander Bot you have to register | ||
To use PyLendar Bot you have to register | ||
your Telegram Id in your profile page. | ||
|
||
Your Id is {chat.user_id} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
from typing import Any, Dict, Optional | ||
|
||
from httpx import AsyncClient | ||
import requests | ||
|
||
|
||
class Chat: | ||
|
@@ -27,6 +26,8 @@ def _get_first_name(self, data: Dict) -> str: | |
|
||
|
||
class Bot: | ||
MEMORY = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is problematic. It's equivalent to a global variable and might create problems when working asynchronously. |
||
|
||
def __init__(self, bot_api: str, webhook_url: str): | ||
self.base = self._set_base_url(bot_api) | ||
self.webhook_setter_url = self._set_webhook_setter_url(webhook_url) | ||
|
@@ -37,12 +38,14 @@ def _set_base_url(self, bot_api: str) -> str: | |
def _set_webhook_setter_url(self, webhook_url: str) -> str: | ||
return f'{self.base}setWebhook?url={webhook_url}/telegram/' | ||
|
||
def set_webhook(self): | ||
return requests.get(self.webhook_setter_url) | ||
async def set_webhook(self): | ||
async with AsyncClient() as ac: | ||
return await ac.get(self.webhook_setter_url) | ||
|
||
def drop_webhook(self): | ||
data = {'drop_pending_updates': True} | ||
return requests.get(url=f'{self.base}deleteWebhook', data=data) | ||
async def drop_webhook(self): | ||
async with AsyncClient() as ac: | ||
data = {'drop_pending_updates': True} | ||
return await ac.post(url=f'{self.base}deleteWebhook', data=data) | ||
|
||
async def send_message( | ||
self, chat_id: str, | ||
|
Binary file not shown.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just
asyncio.run
theset_webhook
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's impossible - asyncio.run() cannot be called from a running event loop (which started by FastAPI).
It seems "ensure_future" is the best way of executing code in asyncio without awaiting and closing the current loop.