I have inline buttons with a response to them, some people spam them, I would like to make a delay for the command.
How to implement this?
If possible, write another way to answer the user that the command can be written after a while
#dp.callback_query_handler(id, text='Правила ⚔️')
#dp.callback_query_handler(id, text='Мануалы 🧸')
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
answer_data = query.data
# always answer callback queries, even if you have nothing to say
await query.answer(f'Вы выбрали пункт {answer_data!r}')
if answer_data == 'Помощь 🧞':
text = "Есть какие-то вопросы? 🛎\n \nПо вопросам внутри чата:\n \n Сотрудничество - #reimannlive 🈺 \n \nПо всем вопросам: #FollHash ☯️\nПо всем вопросам, заявкам в тиму: #t3sse ☯️\n \nВот полезные команды, для развлекухи:\n \n/cat - киски 😏\n/dog - собачки 🦮\n/music - музычка 🌆\n/robot - бредовый видосик 🤣"
elif answer_data == 'Правила ⚔️':
text = """Пᴩᴀʙиᴧᴀ чᴀᴛᴀ 💻
=======================
Зᴀᴨᴩᴇщᴇнᴏ: 🚫
- ᴧюбᴀя ᴋᴏʍʍᴇᴩция ʙ чᴀᴛᴇ (ᴨᴏᴋуᴨᴋᴀ/ᴨᴩᴏдᴀжᴀ)
- уᴋᴀɜыʙᴀᴛь иᴧи ᴨᴏʍᴇчᴀᴛь дᴩуᴦиᴇ ᴋᴀнᴀᴧы иᴧи бᴏᴛы
- ᴩᴇᴋᴧᴀʍᴀ иᴧи уᴨᴏʍинᴀниᴇ ᴨᴏхᴏжих ᴩᴇᴄуᴩᴄᴏʙ/ɯᴏᴨᴏʙ/нᴇйʍᴏʙ ʙ ᴧюбᴏʍ ᴋᴏнᴛᴇᴋᴄᴛᴇ
- ᴨᴏᴨᴩᴏɯᴀйничᴇᴄᴛʙᴏ
- ɜᴧᴏуᴨᴏᴛᴩᴇбᴧᴇниᴇ "CAPS LOCK"
- ʙᴇᴄᴛи ᴄᴇбя нᴇᴀдᴇᴋʙᴀᴛнᴏ ʙ чᴀᴛᴇ и ᴩᴀɜʙᴏдиᴛь "ᴄᴩᴀч"
- ᴏᴄᴋᴏᴩбᴧᴇниᴇ "мᴏдᴇᴩᴀции/ᴨᴩᴏᴇᴋᴛᴀ/ɯᴏᴨᴀ" - бᴀн ❗️
- ᴏᴛᴨᴩᴀʙᴧяᴛь ᴄᴋᴩиʍᴇᴩы, ᴩᴀᴄчᴧᴇнᴇнᴋу, ᴄʙᴀᴄᴛиᴋу, нᴀциɜʍ, ᴋᴏнᴛᴇнᴛ 🔞
- ᴏɸᴏᴩʍᴧяᴛь ᴩᴀɜᴧичныᴇ ᴋᴀᴩᴛы, ᴀбуɜиᴛь ᴩᴇɸᴇᴩᴀᴧьную ᴄиᴄᴛᴇʍу, ᴄᴋᴀʍ и ᴏбʍᴀн ᴨᴏᴧьɜᴏʙᴀᴛᴇᴧᴇй
- ᴨᴩᴏᴨᴀᴦᴀндᴀ ᴨᴏᴧиᴛиᴋи
- ɸᴧуд\ᴄᴨᴀʍ ᴏдинᴀᴋᴏʙыʍи ɜᴀ ᴋᴏнᴛᴇᴋᴄᴛᴏʍ ᴄᴧᴏʙᴀʍи иᴧи ᴨᴩᴇдᴧᴏжᴇнияʍи (1 ᴨᴩᴇдуᴨᴩᴇждᴇниᴇ, ᴨᴏᴄᴧᴇ - ɯᴛᴩᴀɸ) """
elif answer_data == 'Мануалы 🧸':
text = "Краткий мануал о том как обрабатывать логи - https://telegra.ph/Kak-obrabatyvat-logi-05-30\nЗа привлечение новой аудитории, выдаю логи🥳"
else:
text = f'Unexpected callback data {answer_data!r}!'
await bot.send_message(ID, text)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)
I hope you can help me, thanks in advance!
aiogram has a flood avoidance (anti flood) example, which basically prevents users from misusing the command. remember to install (aiohttp, aioredis and redis) on your server.
import asyncio
from aiogram import Bot, Dispatcher, executor, types
from aiogram.contrib.fsm_storage.redis import RedisStorage2
from aiogram.dispatcher import DEFAULT_RATE_LIMIT
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
TOKEN = 'BOT_TOKEN_HERE'
# In this example Redis storage is used
storage = RedisStorage2(db=5)
bot = Bot(token=TOKEN)
dp = Dispatcher(bot, storage=storage)
def rate_limit(limit: int, key=None):
"""
Decorator for configuring rate limit and key in different functions.
:param limit:
:param key:
:return:
"""
def decorator(func):
setattr(func, 'throttling_rate_limit', limit)
if key:
setattr(func, 'throttling_key', key)
return func
return decorator
class ThrottlingMiddleware(BaseMiddleware):
"""
Simple middleware
"""
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'):
self.rate_limit = limit
self.prefix = key_prefix
super(ThrottlingMiddleware, self).__init__()
async def on_process_message(self, message: types.Message, data: dict):
"""
This handler is called when dispatcher receives a message
:param message:
"""
# Get current handler
handler = current_handler.get()
# Get dispatcher from context
dispatcher = Dispatcher.get_current()
# If handler was configured, get rate limit and key from handler
if handler:
limit = getattr(handler, 'throttling_rate_limit', self.rate_limit)
key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}")
else:
limit = self.rate_limit
key = f"{self.prefix}_message"
# Use Dispatcher.throttle method.
try:
await dispatcher.throttle(key, rate=limit)
except Throttled as t:
# Execute action
await self.message_throttled(message, t)
# Cancel current handler
raise CancelHandler()
async def message_throttled(self, message: types.Message, throttled: Throttled):
"""
Notify user only on first exceed and notify about unlocking only on last exceed
:param message:
:param throttled:
"""
handler = current_handler.get()
dispatcher = Dispatcher.get_current()
if handler:
key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}")
else:
key = f"{self.prefix}_message"
# Calculate how many time is left till the block ends
delta = throttled.rate - throttled.delta
# Prevent flooding
if throttled.exceeded_count <= 2:
await message.reply('Too many requests! ')
# Sleep.
await asyncio.sleep(delta)
# Check lock status
thr = await dispatcher.check_key(key)
# If current message is not last with current key - do not send message
if thr.exceeded_count == throttled.exceeded_count:
await message.reply('Unlocked.')
#dp.message_handler(commands=['start'])
#rate_limit(5, 'start') # this is not required but you can configure throttling manager for current handler using it
async def cmd_test(message: types.Message):
# You can use this command every 5 seconds
await message.reply('Test passed! You can use this command every 5 seconds.')
if __name__ == '__main__':
# Setup middleware
dp.middleware.setup(ThrottlingMiddleware())
# Start long-polling
executor.start_polling(dp)
Related
i tried to create a telegram bot in aiogram to send messages if a particular share price is reached. im using threading in order to run this in background as the bot takes user inputs. basically it takes a dict and takes the key as ticker for yfinance and gets the current price and compares with the given condition and if its a true condition it will send the alert to a chat. this is the code
#bot info
import yfinance as yf
import logging
logging.basicConfig(level=logging.INFO)
from aiogram import Bot, Dispatcher, executor
import asyncio
import threading
bot = Bot(token='bot api key')
dp = Dispatcher(bot)
sh={}
jk={}
#reads a dict from a text file
with open('shares.txt','r') as f:
a=f.read()
b=a.rstrip('}').lstrip('{').split(',')
for i in b:
try:
c=i.split(':')
com_name=c[0].strip().strip(''' ' ''').strip()
com_price=float(c[1])
sh[com_name]=com_price
except:
sh={}
break
async def send_price(pp: str):
await bot.send_message(chat_id=user chat id, text=str(pp))
#price updater
def pp():
while True:
pl = sh.copy()
index = ''
for keys, values in list(pl.items()):
if keys in jk and pl[keys] == jk[keys]:
del pl[keys]
print(pl)
kj = pl
if len(kj)==0:
break
for i in kj:
if kj[i]==0:
break
s = yf.Ticker(i)
a = (s.info)['currentPrice']
# print(s,a)
if kj[i] == 0:
continue
# print('kj',kj[i])
if kj[i] <= a:
index += (f'{i} is currently up rn up with current price being {str(a)} \n')
jk[i] = kj[i]
#print(index)
if len(index) != 0:
asyncio.run(send_price(index))
threading.Thread(target=pp).start()
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)
when i run the code i get this error .
RuntimeError: Timeout context manager should be used inside a task
I have a script, it has inline buttons that, when clicked in a group chat, send a message to private messages.
I would like to try so that they send in response to pressing the inline button by chat id
If possible, write me where and how to add sending a message via chat id
For these messages, I need to change the sending to the chat id: https://imgur.com/a/RdXSb7E
PS: here is the chat id (-1001479485376)
import logging
import asyncio
from aiogram import Bot, Dispatcher, executor, types
from aiogram.utils.markdown import hbold, hunderline, hcode, hlink
API_TOKEN = 'token'
# Configure logging
logging.basicConfig(level=logging.INFO)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
#dp.message_handler(commands='cat')
async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Вы хотите видеть много кисок? Вы готовы? 😏")
# Wait a little...
await asyncio.sleep(1)
# Good bots should send chat actions...
await types.ChatActions.upload_photo()
# Create media group
media = types.MediaGroup()
# Attach local file
media.attach_photo(types.InputFile('data/cat.jpg'), 'Cat!')
# More local files and more cats!
media.attach_photo(types.InputFile('data/cats.jpg'), 'More cats!')
# You can also use URL's
# For example: get random puss:
media.attach_photo('http://lorempixel.com/400/200/cats/', 'Random cat.')
# And you can also use file ID:
# media.attach_photo('<file_id>', 'cat-cat-cat.')
# Done! Send media group
await message.reply_media_group(media=media)
#dp.message_handler(commands='dog')
async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Вы хотите видеть много собачек? Вы готовы? 😏")
# Wait a little...
await asyncio.sleep(1)
# Good bots should send chat actions...
await types.ChatActions.upload_photo()
# Create media group
media = types.MediaGroup()
# Attach local file
media.attach_photo(types.InputFile('data/c2b.jpg'), 'Dog!')
# More local files and more cats!
media.attach_photo(types.InputFile('data/c2a.jpg'), 'More dog!')
media.attach_photo(types.InputFile('data/c2w.gif'), 'More dogs!')
# Done! Send media group
await message.reply_media_group(media=media)
#dp.message_handler(commands='music')
async def send_welcome(message: types.Message):
# So... At first I want to send something like this:
await message.reply("Вот и музычка подъехала! Вы готовы? 😏")
# Wait a little...
await asyncio.sleep(1)
await types.ChatActions.upload_photo()
# Create media group
media = types.MediaGroup()
media.attach_audio(types.InputFile('data/ChillOut.mp3'), 'Music!')
media.attach_audio(types.InputFile('data/REIMANN TEAM.mp3'), 'Music2!')
media.attach_audio(types.InputFile('data/Mister Robot Поёт.mp3'), 'Music3!')
# Done! Send media group
await message.reply_media_group(media=media)
#dp.message_handler(commands='bot')
async def start_cmd_handler(message: types.Message):
keyboard_markup = types.InlineKeyboardMarkup(row_width=3)
# default row_width is 3, so here we can omit it actually
# kept for clearness
text_and_data = (
('Помощь 🧞', 'Помощь 🧞'),
('Правила ⚔️', 'Правила ⚔️'),
('Мануалы 🧸', 'Мануалы 🧸'),
)
# in real life for the callback_data the callback data factory should be used
# here the raw string is used for the simplicity
row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data)
keyboard_markup.row(*row_btns)
keyboard_markup.add(
# url buttons have no callback data
types.InlineKeyboardButton('ReimannLogs 🔱', url='https://t.me/reimannlogs_bot'),
)
await message.reply("Здравствуй, сударь! 👋🏻\nС чем тебе нужна помощь?", reply_markup=keyboard_markup)
# Use multiple registrators. Handler will execute when one of the filters is OK
#dp.callback_query_handler(text='Правила ⚔️') # if cb.data == 'no'
#dp.callback_query_handler(text='Помощь 🧞') # if cb.data == 'yes'
#dp.callback_query_handler(text='Мануалы 🧸')
async def inline_kb_answer_callback_handler(query: types.CallbackQuery):
answer_data = query.data
# always answer callback queries, even if you have nothing to say
await query.answer(f'Вы выбрали пункт {answer_data!r}')
if answer_data == 'Помощь 🧞':
text = "Есть какие-то вопросы? 🛎\n \nВот реквезиты:\n \nПо всем вопросам: #FollHash ☯️\nПо всем вопросам, заявкам в тиму: #t3sse ☯️\n \nВот полезные команды, для развлекухи:\n \n/cat - киски 😏\n/dog - собачки 🦮\n/music - музычка 🌆"
elif answer_data == 'Правила ⚔️':
text = """Пᴩᴀʙиᴧᴀ чᴀᴛᴀ 💻
=======================
Зᴀᴨᴩᴇщᴇнᴏ: 🚫
- ᴧюбᴀя ᴋᴏʍʍᴇᴩция ʙ чᴀᴛᴇ (ᴨᴏᴋуᴨᴋᴀ/ᴨᴩᴏдᴀжᴀ)
- уᴋᴀɜыʙᴀᴛь иᴧи ᴨᴏʍᴇчᴀᴛь дᴩуᴦиᴇ ᴋᴀнᴀᴧы иᴧи бᴏᴛы
- ᴩᴇᴋᴧᴀʍᴀ иᴧи уᴨᴏʍинᴀниᴇ ᴨᴏхᴏжих ᴩᴇᴄуᴩᴄᴏʙ/ɯᴏᴨᴏʙ/нᴇйʍᴏʙ ʙ ᴧюбᴏʍ ᴋᴏнᴛᴇᴋᴄᴛᴇ
- ᴨᴏᴨᴩᴏɯᴀйничᴇᴄᴛʙᴏ
- ɜᴧᴏуᴨᴏᴛᴩᴇбᴧᴇниᴇ "CAPS LOCK"
- ʙᴇᴄᴛи ᴄᴇбя нᴇᴀдᴇᴋʙᴀᴛнᴏ ʙ чᴀᴛᴇ и ᴩᴀɜʙᴏдиᴛь "ᴄᴩᴀч"
- ᴏᴄᴋᴏᴩбᴧᴇниᴇ "мᴏдᴇᴩᴀции/ᴨᴩᴏᴇᴋᴛᴀ/ɯᴏᴨᴀ" - бᴀн ❗️
- ᴏᴛᴨᴩᴀʙᴧяᴛь ᴄᴋᴩиʍᴇᴩы, ᴩᴀᴄчᴧᴇнᴇнᴋу, ᴄʙᴀᴄᴛиᴋу, нᴀциɜʍ, ᴋᴏнᴛᴇнᴛ 🔞
- ᴏɸᴏᴩʍᴧяᴛь ᴩᴀɜᴧичныᴇ ᴋᴀᴩᴛы, ᴀбуɜиᴛь ᴩᴇɸᴇᴩᴀᴧьную ᴄиᴄᴛᴇʍу, ᴄᴋᴀʍ и ᴏбʍᴀн ᴨᴏᴧьɜᴏʙᴀᴛᴇᴧᴇй
- ᴨᴩᴏᴨᴀᴦᴀндᴀ ᴨᴏᴧиᴛиᴋи
- ɸᴧуд\ᴄᴨᴀʍ ᴏдинᴀᴋᴏʙыʍи ɜᴀ ᴋᴏнᴛᴇᴋᴄᴛᴏʍ ᴄᴧᴏʙᴀʍи иᴧи ᴨᴩᴇдᴧᴏжᴇнияʍи (1 ᴨᴩᴇдуᴨᴩᴇждᴇниᴇ, ᴨᴏᴄᴧᴇ - ɯᴛᴩᴀɸ) """
elif answer_data == 'Мануалы 🧸':
text = "Краткий мануал о том как обрабатывать логи - https://telegra.ph/Kak-obrabatyvat-logi-05-30\nЗа привлечение новой аудитории, выдаю логи🥳"
else:
text = f'Unexpected callback data {answer_data!r}!'
await bot.send_message(query.from_chat.id, text)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)
Just change await bot.send_message(query.from_chat.id, text) to await bot.send_message(-1001479485376, text). See the docs here.
I am trying to build agents programmatically and have been following this example. And I could see topics and agents when I look at faust -A <> agents that have been generated at the start of the script. The agent name is something like {topic}_agent as per example. All these agents respectively reach out to some rest api. Here is the code.
def create_agent(next_topic, model_metadata: Dict):
"""
creation of a single agent.
`start_topic`: str
Just a string that you can use in other functions
to figure out how messages in that topic can be
transformed
`next_topic`: faust.topics.Topic
A faust `app.topic` instance
"""
async def agent(stream):
"""Call domino model and return response
"""
async for message in stream:
domino_req = {"data": message.asdict()}
app.logger.info(domino_req)
response = requests.post(
model_metadata['url'],
auth=(
model_metadata['auth'],
model_metadata['auth'],
),
json=domino_req,
)
if response.status_code == 200:
answer = response.json()
await next_topic.send(answer['result'])
else:
app.logger.error(response.reason)
# app.logger.info(f"NEW Agent Created: ## Agent - {consumer} ##")
return agent
def agents(registry_response):
"""
configuration of multiple agents
"""
agents = []
for key, value in registry_response.items():
""" `topic`: app.topic instance """
agents.append(
# `topic.start`: str
# `topic.next`: faust.topics.Topic
(create_agent(next_topic= all_responses, model_metadata = value),key)
)
return agents
def attach_agent(agent, topic):
""" Attach the agent to the Faust app """
# `topic.faust`: faust.topics.Topic
# it is equivalent to `app.topic(topic.start)`
print("hello")
app.agent(channel=app.topic(topic), name=f"{topic}_agent")(agent)
# new_agent.start()
# app.logger.info("hello")
# app.logger.info(new_agent)
# app.logger.info(new_agent.info())
# app.logger.info(app.agents)
# #app.task
# async def get_model_registry():
# """
# Create topics and agents for the initial set of models present in the registry
# """
app.logger.info('APP STARTED')
app.logger.info('Fetching Models from Model Registry')
#TODO: Call the Model Registry and process it
#Just mocking the registry
registry_response = initial_model_registry_metadata
app.logger.info(f'Number of Models Found {len(registry_response)}')
for agent,topic in agents(registry_response):
attach_agent(agent, topic)
#app.page('/model/{key_ai_model}')
class frontdoor(View):
async def get(self, request: Request) -> Response:
return self.json({'key_ai_model': key_ai_model})
async def post(self, request: Request, key_ai_model: str) -> Response:
request_id = str(uuid.uuid4())
src = await request.json()
msg = GdeltRequest(**src)
app.logger.info(msg)
await gdelt_agent.cast(msg, key=request_id)
return self.json({'request_id': request_id, 'key_ai_model': key_ai_model})
#app.agent(all_responses)
async def print_responses(stream):
async for message in stream:
print(message)
However, when I post something I am getting an error - NameError: name 'gdelt_agent' is not defined. Any help here will be much appreciated.
I have been able to figure this out by assigning the agents as a value in dicitonary and later using it to cast messages to the agent.
agents_dict = {}
topics_dict ={}
def attach_agent(agent, topic):
""" Attach the agent to the Faust app """
# `topic.faust`: faust.topics.Topic
# it is equivalent to `app.topic(topic.start)`
print("hello")
topics_dict[topic] = app.topic(topic)
agents_dict[topic] = app.agent(channel=topics_dict[topic], name=f"{topic}_agent")(agent)
agents_dict[topic].start()
Thank you everyone.
I'm working with faust and would like to leverage concurrency feature.
The example listed doesn't quite demonstrate the use of concurrency.
What I would like to do is, read from kafka producer and unnest json.
Then the shipments are sent to a process to calculate billing etc. I should send 10 shipments at one time to a function which does the calculation. For this i'm using concurrency so 10 shipments can calculate concurrently.
import faust
import time
import json
from typing import List
import asyncio
class Items(faust.Record):
name: str
billing_unit: str
billing_qty: int
class Shipments(faust.Record, serializer="json"):
shipments: List[Items]
ship_type: str
shipping_service: str
shipped_at: str
app = faust.App('ships_app', broker='kafka://localhost:9092', )
ship_topic = app.topic('test_shipments', value_type=Shipments)
#app.agent(value_type=str, concurrency=10)
async def mytask(records):
# task that does some other activity
async for record in records:
print(f'received....{record}')
time.sleep(5)
#app.agent(ship_topic)
async def process_shipments(shipments):
# async for ships in stream.take(100, within=10):
async for ships in shipments:
data = ships.items
uid = faust.uuid()
for item in data:
item_uuid = faust.uuid()
print(f'{uid}, {item_uuid}, {ships.ship_type}, {ships.shipping_service}, {ships.shipped_at}, {item.name}, {item.billing_unit}, {item.billing_qty}')
await mytask.send(value=("{} -- {}".format(uid, item_uuid)))
# time.sleep(2)
# time.sleep(10)
if __name__ == '__main__':
app.main()
Ok I figured out how it works. The problem with the example you gave was actually with the time.sleep bit, not the concurrency bit. Below are two silly examples that show how an agent would work with and without concurrency.
import faust
import asyncio
app = faust.App(
'example_app',
broker="kafka://localhost:9092",
value_serializer='raw',
)
t = app.topic('topic_1')
# #app.agent(t, concurrency=1)
# async def my_task(tasks):
# async for my_task in tasks:
# val = my_task.decode('utf-8')
# if (val == "Meher"):
# # This will print out second because there is only one thread.
# # It'll take 5ish seconds and print out right after Waldo
# print("Meher's a jerk.")
# else:
# await asyncio.sleep(5)
# # Since there's only one thread running this will effectively
# # block the agent.
# print(f"Where did {val} go?")
#app.agent(t, concurrency=2)
async def my_task2(tasks):
async for my_task in tasks:
val = my_task.decode('utf-8')
if (val == "Meher"):
# This will print out first even though the Meher message is
# received second.
print("Meher's a jerk.")
else:
await asyncio.sleep(5)
# Because this will be sleeping and there are two threads available.
print(f"Where did {val} go?")
# ===============================
# In another process run
from kafka import KafkaProducer
p = KafkaProducer()
p.send('topic_1', b'Waldo'); p.send('topic_1', b'Meher')
I have a Django project with Tornado websocket opened and is subscribed to a topic in my Redis Pub/Sub. I am using asyncio and aioredis. Before page refresh, browser close or navigation out of that page, I will call for the websocket to close, which will unsubscribe from that topic.
The issue here is sometimes when I change pages or refresh the page, a bunch of messages from the past will be pumped back into the newly opened websocket. It doesn't happen every time, and I'm not sure what else I can do to make sure the past messages don't come back on page refresh. I've already made sure the websocket will close and unsubscribe from the topic on page refresh/window unload.
Does Redis Pub/Sub keep old messages somewhere while the client is unsubscribed? And when the client subscribes back to the same topic, the old messages are sent out? Is this normal behaviour for Redis Pub/Sub? I'm under the impression that Redis Pub/Sub doesn't persist messages and if client is unsubscribed, the messages just get dropped for that client.
I need to make sure when page reloads, the old messages don't get pumped back to the websocket.
This is how I wrote the RedisChannel to execute the pub/sub functions:
import aioredis
class RedisChannel(object):
'''
Redis backed pub-sub websocket channel.
'''
async def subscribe(self, **kwargs):
'''
Subscribe to topics
'''
topics = kwargs.get('topics')
return await self.conn.subscribe(*topics)
async def unsubscribe(self, **kwargs):
'''
Unsubscribe to topics
'''
topics = kwargs.get('topics')
return await self.conn.unsubscribe(*topics)
async def send(self, **kwargs):
data = {}
# If client socket is provided, only send to this socket.
ws = kwargs.get('ws')
# Topic for this message. Compulsory for broadcast.
topic = kwargs.get('topic')
# Usually JSON
if kwargs.get('data'):
data['data'] = kwargs.get('data')
# I'm using 60 seconds right now just to try to limit the list of past messages
# But the behaviour I need is 0 past messages on page reload in browser
push_event = True
if kwargs.get('timestamp'):
event_timestamp = kwargs.get("timestamp", 0)
data['timestamp'] = event_timestamp
# logger.debug(data)
current_time = timezone.now()
if event_timestamp:
event_dt = get_utc_time(datetime.utcfromtimestamp(event_timestamp))
if event_dt:
time_apart = current_time - event_dt
duration = abs(time_apart.total_seconds())
logger.debug("Time apart between event and current time = {}".format(duration))
if duration >= 60:
push_event = False
if not push_event:
data = {}
return await self.conn.publish_json(topic, json.dumps(data, separators=(',', ': ')))
async def connect(self):
redis_settings = settings['redis']['channel']
self.conn = await aioredis.create_redis_pool(
(
redis_settings.get('host'),
redis_settings.get('port')
),
db=redis_settings.get('db'),
minsize=2,
maxsize=redis_settings.get('max_connections'),
encoding='utf-8'
)
This is how I wrote the websocket handler to subscribe/unsubscribe to a Redis topic:
import asyncio, json
ws_channel = RedisChannel()
asyncio.get_event_loop().create_task(ws_channel.connect())
async def reader(ch, ws):
while (await ch.wait_message()):
data = await ch.get_json()
if data:
ws.write_message(data)
await asyncio.sleep(0.001)
# time.sleep
class ResultsWsHandler(tornado.websocket.WebSocketHandler):
def open(self):
try:
self.write_message(json.dumps('Websocket opened.'))
except Exception as e:
logger.error(str(e))
def on_message(self, message):
asyncio.ensure_future(self.on_message_async(message))
async def on_message_async(self, message):
# async def on_message(self, message):
data = json.loads(message)
action = data.get('action', None)
topics = data.get('cameras', [])
if topics or action is not None:
try:
action = int(action)
if action == 0: # 0 - heartbeat
logger.debug('Heartbeat.')
param = {'type': 0}
self.write_message(json.dumps(param))
elif action == 1: # 1 - subscribe
channels = await ws_channel.subscribe(topics=topics)
logger.debug(f'Successfully subscribed from {topics}.')
self.write_message(json.dumps(f'Successfully subscribed to {topics}.'))
task_list = []
for c in channels:
task_list.append(asyncio.ensure_future(reader(c, self)))
await asyncio.wait(task_list)
elif action == 2: # 2 - unsubscribe
await ws_channel.unsubscribe(topics=topics)
logger.debug(f'Successfully unsubscribe from {topics}.')
self.write_message(json.dumps(f'Successfully unsubscribe from {topics}.'))
else:
logger.debug(f'Other: {data}')
except Exception as e:
logger.error(json.dumps(str(e), separators=(',', ': ')))
self.write_message(json.dumps(str(e), separators=(',', ': ')))