Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
submodules: recursive
- uses: psf/black@stable
with:
options: --check --diff --color -l 120 --exclude docs
options: telegram_ros --check --diff --color -l 120 --exclude docs
26 changes: 24 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,33 @@ name: CI
on: [push, pull_request]

jobs:
matrix:
name: Determine modified packages
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.modified-packages.outputs.packages }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 300
- name: Commit Range
id: commit-range
uses: tue-robotics/tue-env/ci/commit-range@master
- name: Modified packages
id: modified-packages
uses: tue-robotics/tue-env/ci/modified-packages@master
with:
commit-range: ${{ steps.commit-range.outputs.commit-range }}
tue-ci:
name: TUe CI - ${{ github.event_name }}
name: TUe CI - ${{ matrix.package }}
runs-on: ubuntu-latest
needs: matrix
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.matrix.outputs.packages) }}
steps:
- name: TUe CI
uses: tue-robotics/tue-env/ci/main@master
with:
package: ${{ github.event.repository.name }}
package: ${{ matrix.package }}
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "docs"]
path = docs
[submodule "telegram_ros/docs"]
path = telegram_ros/docs
url = https://github.com/tue-robotics/tue_documentation_python.git
branch = master
22 changes: 22 additions & 0 deletions telegram_ros/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.0.2)
project(telegram_ros)
find_package(catkin REQUIRED)

find_package(catkin REQUIRED COMPONENTS
sensor_msgs
std_msgs
)

catkin_python_setup()

catkin_package(
CATKIN_DEPENDS sensor_msgs std_msgs
)

#############
## Testing ##
#############

if (CATKIN_ENABLE_TESTING)
catkin_add_nosetests(test)
endif()
File renamed without changes.
6 changes: 1 addition & 5 deletions package.xml → telegram_ros/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@

<depend>sensor_msgs</depend>
<depend>std_msgs</depend>

<build_depend>message_generation</build_depend>

<build_export_depend>message_runtime</build_export_depend>
<depend>telegram_ros_msgs</depend>

<exec_depend>cv_bridge</exec_depend>
<exec_depend>message_runtime</exec_depend>
<exec_depend>python3-numpy</exec_depend>
<exec_depend>python3-opencv</exec_depend>
<exec_depend version_gte="20.0.0">python3-telegram-bot</exec_depend>
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

import functools

import cv2
Expand All @@ -10,8 +12,8 @@
from std_msgs.msg import String, Header
from telegram import Location, ReplyKeyboardMarkup, Update
from telegram.error import TimedOut
from telegram.ext import Updater, CallbackContext, CommandHandler, MessageHandler, filters
from telegram_ros.msg import Options
from telegram.ext import Application, CallbackContext, CommandHandler, MessageHandler, filters
from telegram_ros_msgs.msg import Options


WHITELIST = "~whitelist"
Expand All @@ -26,18 +28,18 @@ def telegram_callback(callback_function):
"""

@functools.wraps(callback_function)
def wrapper(self, update: Update, context: CallbackContext):
async def wrapper(self, update: Update, context: CallbackContext):
rospy.logdebug("Incoming update from telegram: %s", update)
if self._telegram_chat_id is None:
rospy.logwarn("Discarding message. No active chat_id.")
update.message.reply_text("ROS Bridge not initialized. Type /start to set-up ROS bridge")
await update.message.reply_text("ROS Bridge not initialized. Type /start to set-up ROS bridge")
elif self._telegram_chat_id != update.message.chat_id:
rospy.logwarn("Discarding message. Invalid chat_id")
update.message.reply_text(
await update.message.reply_text(
"ROS Bridge initialized to another chat_id. Type /start to connect to this chat_id"
)
else:
callback_function(self, update, context)
await callback_function(self, update, context)

return wrapper

Expand All @@ -56,7 +58,7 @@ def wrapper(self, msg):
rospy.logerr("ROS Bridge not initialized, dropping message of type %s", msg._type)
else:
try:
callback_function(self, msg)
asyncio.run(callback_function(self, msg))
except TimedOut as e:
rospy.logerr("Telegram timeout: %s", e)

Expand Down Expand Up @@ -94,18 +96,16 @@ def __init__(self, api_token, caption_as_frame_id):

# Telegram IO
self._telegram_chat_id = None
self._telegram_updater = Updater(api_token)
self._telegram_updater.dispatcher.add_error_handler(
self._telegram_app = Application.builder().token(api_token).build()
self._telegram_app.add_error_handler(
lambda _, update, error: rospy.logerr("Update {} caused error {}".format(update, error))
)

self._telegram_updater.dispatcher.add_handler(CommandHandler("start", self._telegram_start_callback))
self._telegram_updater.dispatcher.add_handler(CommandHandler("stop", self._telegram_stop_callback))
self._telegram_updater.dispatcher.add_handler(MessageHandler(filters.TEXT, self._telegram_message_callback))
self._telegram_updater.dispatcher.add_handler(MessageHandler(filters.PHOTO, self._telegram_photo_callback))
self._telegram_updater.dispatcher.add_handler(
MessageHandler(filters.LOCATION, self._telegram_location_callback)
)
self._telegram_app.add_handler(CommandHandler("start", self._telegram_start_callback))
self._telegram_app.add_handler(CommandHandler("stop", self._telegram_stop_callback))
self._telegram_app.add_handler(MessageHandler(filters.TEXT, self._telegram_message_callback))
self._telegram_app.add_handler(MessageHandler(filters.PHOTO, self._telegram_photo_callback))
self._telegram_app.add_handler(MessageHandler(filters.LOCATION, self._telegram_location_callback))

rospy.core.add_preshutdown_hook(self._shutdown)

Expand All @@ -114,25 +114,27 @@ def _shutdown(self, reason: str):
Sending a message to the current chat id on destruction.
"""
if self._telegram_chat_id:
self._telegram_updater.bot.send_message(
self._telegram_chat_id,
f"Stopping Telegram ROS bridge, ending this chat. Reason of shutdown: {reason}."
" Type /start to connect again after starting a new Telegram ROS bridge.",
asyncio.run(
self._telegram_app.bot.send_message(
self._telegram_chat_id,
f"Stopping Telegram ROS bridge, ending this chat. Reason of shutdown: {reason}."
" Type /start to connect again after starting a new Telegram ROS bridge.",
)
)

def spin(self):
"""
Starts the Telegram update thread and spins until a SIGINT is received
"""
self._telegram_updater.start_polling()
self._telegram_app.run_polling() # ToDo: this is blocking
rospy.loginfo("Telegram updater started polling, spinning ..")

rospy.spin()
rospy.loginfo("Shutting down Telegram updater ...")

self._telegram_updater.stop()
self._telegram_app.stop()

def _telegram_start_callback(self, update: Update, _: CallbackContext):
async def _telegram_start_callback(self, update: Update, _: CallbackContext):
"""
Called when a Telegram user sends the '/start' event to the bot, using this event, the bridge can be connected
to a specific conversation.
Expand All @@ -149,7 +151,7 @@ def _telegram_start_callback(self, update: Update, _: CallbackContext):
new_user = "'somebody'"
if hasattr(update.message.chat, "first_name") and update.message.chat.first_name:
new_user = update.message.chat.first_name
self._telegram_updater.bot.send_message(
self._telegram_app.bot.send_message(
self._telegram_chat_id,
"Lost ROS bridge connection to this chat_id {} ({} took over)".format(
update.message.chat_id, new_user
Expand All @@ -159,17 +161,17 @@ def _telegram_start_callback(self, update: Update, _: CallbackContext):
rospy.loginfo("Starting Telegram ROS bridge for new chat id {}".format(update.message.chat_id))
self._telegram_chat_id = update.message.chat_id

update.message.reply_text(
await update.message.reply_text(
"Telegram ROS bridge initialized, only replying to chat_id {} (current)".format(self._telegram_chat_id)
)
else:
rospy.logwarn("Discarding message. User {} not whitelisted".format(update.message.from_user))
update.message.reply_text(
await update.message.reply_text(
"You (user id {}) are not authorized to chat with this bot".format(update.message.from_user.id)
)

@telegram_callback
def _telegram_stop_callback(self, update: Update, _: CallbackContext):
async def _telegram_stop_callback(self, update: Update, _: CallbackContext):
"""
Called when a Telegram user sends the '/stop' event to the bot. Then, the user is disconnected from the bot and
will no longer receive messages.
Expand All @@ -178,14 +180,14 @@ def _telegram_stop_callback(self, update: Update, _: CallbackContext):
"""

rospy.loginfo("Stopping Telegram ROS bridge for chat id {}".format(self._telegram_chat_id))
update.message.reply_text(
"Disconnecting chat_id {}. So long and thanks for all the fish!"
" Type /start to reconnect".format(self._telegram_chat_id)
await update.message.reply_text(
f"Disconnecting chat_id {self._telegram_chat_id}. So long and thanks for all the fish!"
" Type /start to reconnect"
)
self._telegram_chat_id = None

@telegram_callback
def _telegram_message_callback(self, update: Update, _: CallbackContext):
async def _telegram_message_callback(self, update: Update, _: CallbackContext):
"""
Called when a new Telegram message has been received. The method will verify whether the incoming message is
from the bridges Telegram conversation by comparing the chat_id.
Expand All @@ -196,27 +198,28 @@ def _telegram_message_callback(self, update: Update, _: CallbackContext):
self._from_telegram_string_publisher.publish(String(data=text))

@ros_callback
def _ros_string_callback(self, msg: String):
async def _ros_string_callback(self, msg: String):
"""
Called when a new ROS String message is coming in that should be sent to the Telegram conversation

:param msg: String message
"""
if msg.data:
self._telegram_updater.bot.send_message(self._telegram_chat_id, msg.data)
await self._telegram_app.bot.send_message(self._telegram_chat_id, msg.data)
else:
rospy.logwarn("Ignoring empty string message")

@telegram_callback
def _telegram_photo_callback(self, update: Update, _: CallbackContext):
async def _telegram_photo_callback(self, update: Update, _: CallbackContext):
"""
Called when a new Telegram photo has been received. The method will verify whether the incoming message is
from the bridges Telegram conversation by comparing the chat_id.

:param update: Received update that holds the chat_id and message data
"""
rospy.logdebug("Received image, downloading highest resolution image ...")
byte_array = update.message.photo[-1].get_file().download_as_bytearray()
new_file = await update.message.photo[-1].get_file()
byte_array = await new_file.download_as_bytearray()
rospy.logdebug("Download complete, publishing ...")

img = cv2.imdecode(np.asarray(byte_array, dtype=np.uint8), cv2.IMREAD_COLOR)
Expand All @@ -231,21 +234,21 @@ def _telegram_photo_callback(self, update: Update, _: CallbackContext):
self._from_telegram_string_publisher.publish(String(data=update.message.caption))

@ros_callback
def _ros_image_callback(self, msg: Image):
async def _ros_image_callback(self, msg: Image):
"""
Called when a new ROS Image message is coming in that should be sent to the Telegram conversation

:param msg: Image message
"""
cv2_img = self._cv_bridge.imgmsg_to_cv2(msg, "bgr8")
self._telegram_updater.bot.send_photo(
await self._telegram_app.bot.send_photo(
self._telegram_chat_id,
photo=BytesIO(cv2.imencode(".jpg", cv2_img)[1].tobytes()),
caption=msg.header.frame_id,
)

@telegram_callback
def _telegram_location_callback(self, update: Update, _: CallbackContext):
async def _telegram_location_callback(self, update: Update, _: CallbackContext):
"""
Called when a new Telegram Location is received. The method will verify whether the incoming Location is
from the bridged Telegram conversation by comparing the chat_id.
Expand All @@ -262,16 +265,18 @@ def _telegram_location_callback(self, update: Update, _: CallbackContext):
)

@ros_callback
def _ros_location_callback(self, msg: NavSatFix):
async def _ros_location_callback(self, msg: NavSatFix):
"""
Called when a new ROS NavSatFix message is coming in that should be sent to the Telegram conversation

:param msg: NavSatFix that the robot wants to share
"""
self._telegram_updater.bot.send_location(self._telegram_chat_id, location=Location(msg.longitude, msg.latitude))
await self._telegram_app.bot.send_location(
self._telegram_chat_id, location=Location(msg.longitude, msg.latitude)
)

@ros_callback
def _ros_options_callback(self, msg: Options):
async def _ros_options_callback(self, msg: Options):
"""
Called when a new ROS Options message is coming in that should be sent to the Telegram conversation

Expand All @@ -283,7 +288,7 @@ def chunks(l, n): # noqa: E741
for i in range(0, len(l), n):
yield l[i : i + n] # noqa: E203

self._telegram_updater.bot.send_message(
await self._telegram_app.bot.send_message(
self._telegram_chat_id,
text=msg.question,
reply_markup=ReplyKeyboardMarkup(
Expand Down
File renamed without changes.
17 changes: 2 additions & 15 deletions CMakeLists.txt → telegram_ros_msgs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
cmake_minimum_required(VERSION 3.0.2)
project(telegram_ros)
project(telegram_ros_msgs)
find_package(catkin REQUIRED)

find_package(catkin REQUIRED COMPONENTS
message_generation
sensor_msgs
std_msgs
)

catkin_python_setup()

# Generate messages in the 'msg' folder
add_message_files(
FILES
Expand All @@ -19,18 +15,9 @@ add_message_files(
# Generate added messages and services with any dependencies listed here
generate_messages(
DEPENDENCIES
sensor_msgs
std_msgs
)

catkin_package(
CATKIN_DEPENDS message_runtime sensor_msgs std_msgs
CATKIN_DEPENDS message_runtime
)

#############
## Testing ##
#############

if (CATKIN_ENABLE_TESTING)
catkin_add_nosetests(test)
endif()
File renamed without changes.
22 changes: 22 additions & 0 deletions telegram_ros_msgs/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<?xml-model
href="http://download.ros.org/schema/package_format3.xsd"
schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>telegram_ros_msgs</name>
<version>0.0.0</version>
<description>The telegram_ros_msgs package</description>

<maintainer email="[email protected]">Rein Appeldoorn</maintainer>

<license>MIT</license>

<buildtool_depend>catkin</buildtool_depend>

<build_depend>message_generation</build_depend>

<build_export_depend>message_runtime</build_export_depend>

<exec_depend>message_runtime</exec_depend>

</package>