Skip to content

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 37 commits into from
Feb 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
12cc75c
Minor changes
leddcode Jan 20, 2021
f5f29c0
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode Jan 20, 2021
a7793c3
Add telegram client
leddcode Jan 21, 2021
bfe2ceb
Apply merge changes
leddcode Jan 21, 2021
7af4147
Minor fixes
leddcode Jan 21, 2021
8433c4a
Minor fixes
leddcode Jan 21, 2021
091db8e
Create FastAPI Settings object
leddcode Jan 22, 2021
8e93a36
Use status code names in telegram tests
leddcode Jan 22, 2021
b8d0cba
Use status code names in telegram tests
leddcode Jan 22, 2021
198e4d6
Add Asynchronous Testing and Increase Code Coverage.
leddcode Jan 23, 2021
3fc9d49
Added keyboards, handlers and tests. Updated telegram models.
leddcode Jan 23, 2021
798a72f
Update tests.
leddcode Jan 24, 2021
3078682
Minor fixes.
leddcode Jan 24, 2021
94a8c01
Add test bot client with registered user
leddcode Jan 24, 2021
b0bc592
Minor fix
leddcode Jan 24, 2021
de2995c
Make telegram bot working async. Add async fixture and tests.
leddcode Jan 24, 2021
e618089
Resolve conflict on merge
leddcode Jan 25, 2021
2122222
Add telegram tests
leddcode Jan 25, 2021
c1d309a
Make request.post works asynchronous
leddcode Jan 27, 2021
8795a7b
Merge updates
leddcode Jan 27, 2021
a5b4a6f
Minor change
leddcode Jan 27, 2021
08e65de
Merge updates to telegram
leddcode Jan 29, 2021
6229fc0
Add an ability to create events on telegram
leddcode Jan 31, 2021
f86f5a1
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode Jan 31, 2021
245c0c0
Merge updates from develop
leddcode Jan 31, 2021
316c3ab
Test creating new events
leddcode Jan 31, 2021
5dcc8ac
Minor fix
leddcode Jan 31, 2021
2bc356a
Merge changes
leddcode Feb 1, 2021
68d75d2
Splitting to functions
leddcode Feb 1, 2021
a9d8205
Merge updates
leddcode Feb 2, 2021
28fe8e8
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
leddcode Feb 5, 2021
2ff5de3
Minor changes
leddcode Feb 5, 2021
2a20efc
Merge changes
leddcode Feb 6, 2021
2086e56
Set and drop webhook asynchronously
leddcode Feb 7, 2021
73cb88c
Merge requirements.txt
leddcode Feb 7, 2021
1137770
Merge requirements.txt
leddcode Feb 7, 2021
498ef67
Minor fix
leddcode Feb 7, 2021
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
3 changes: 0 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
agenda, calendar, categories, dayview, email,
event, invitation, profile, search, telegram, whatsapp
)
from app.telegram.bot import telegram_bot


def create_tables(engine, psql_environment):
Expand Down Expand Up @@ -51,8 +50,6 @@ def create_tables(engine, psql_environment):
for router in routers_to_include:
app.include_router(router)

telegram_bot.set_webhook()


# TODO: I add the quote day to the home page
# until the relevant calendar view will be developed.
Expand Down
9 changes: 1 addition & 8 deletions app/routers/telegram.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Body, Depends, Request
from fastapi import APIRouter, Body, Depends

from app.database.database import get_db
from app.database.models import User
Expand All @@ -13,13 +13,6 @@
)


@router.get("/")
async def telegram(request: Request, session=Depends(get_db)):

# todo: Add templating
return "Start using PyLander telegram bot!"


@router.post("/")
async def bot_client(req: dict = Body(...), session=Depends(get_db)):
chat = Chat(req)
Expand Down
6 changes: 6 additions & 0 deletions app/telegram/bot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

from app import config
from app.dependencies import get_settings
from .models import Bot
Expand All @@ -9,3 +11,7 @@
WEBHOOK_URL = settings.webhook_url

telegram_bot = Bot(BOT_API, WEBHOOK_URL)

loop = asyncio.get_event_loop()
Copy link
Member

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 the set_webhook

Copy link
Contributor Author

@leddcode leddcode Feb 7, 2021

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.

asyncio.set_event_loop(loop)
asyncio.ensure_future(telegram_bot.set_webhook())
197 changes: 176 additions & 21 deletions app/telegram/handlers.py
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:
Expand All @@ -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

Expand All @@ -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()

Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The 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}
Expand Down
16 changes: 16 additions & 0 deletions app/telegram/keyboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
]
]

new_event_buttons = [
[
{'text': 'Create ✅', 'callback_data': 'create'},
{'text': 'Cancel 🚫', 'callback_data': 'cancel'}
]
]

field_buttons = [
[
{'text': 'Restart 🚀', 'callback_data': 'restart'},
{'text': 'Cancel 🚫', 'callback_data': 'cancel'}
]
]

DATE_FORMAT = '%d %b %Y'


Expand Down Expand Up @@ -45,3 +59,5 @@ def gen_inline_keyboard(buttons: List[List[Any]]) -> Dict[str, Any]:


show_events_kb = gen_inline_keyboard(show_events_buttons)
new_event_kb = gen_inline_keyboard(new_event_buttons)
field_kb = gen_inline_keyboard(field_buttons)
15 changes: 9 additions & 6 deletions app/telegram/models.py
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:
Expand All @@ -27,6 +26,8 @@ def _get_first_name(self, data: Dict) -> str:


class Bot:
MEMORY = {}
Copy link
Member

Choose a reason for hiding this comment

The 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)
Expand All @@ -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,
Expand Down
Binary file modified requirements.txt
Binary file not shown.
Loading