diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5f2c44b..fef64e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f3af87..ddffe0a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 }} diff --git a/.gitmodules b/.gitmodules index 9c4ee42..6f5f00f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/telegram_ros/CMakeLists.txt b/telegram_ros/CMakeLists.txt new file mode 100644 index 0000000..19a729d --- /dev/null +++ b/telegram_ros/CMakeLists.txt @@ -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() diff --git a/README.md b/telegram_ros/README.md similarity index 100% rename from README.md rename to telegram_ros/README.md diff --git a/docs b/telegram_ros/docs similarity index 100% rename from docs rename to telegram_ros/docs diff --git a/package.xml b/telegram_ros/package.xml similarity index 85% rename from package.xml rename to telegram_ros/package.xml index 8d909d0..9e20e4d 100644 --- a/package.xml +++ b/telegram_ros/package.xml @@ -15,13 +15,9 @@ sensor_msgs std_msgs - - message_generation - - message_runtime + telegram_ros_msgs cv_bridge - message_runtime python3-numpy python3-opencv python3-telegram-bot diff --git a/rosdoc.yaml b/telegram_ros/rosdoc.yaml similarity index 100% rename from rosdoc.yaml rename to telegram_ros/rosdoc.yaml diff --git a/scripts/telegram_ros_bridge b/telegram_ros/scripts/telegram_ros_bridge similarity index 100% rename from scripts/telegram_ros_bridge rename to telegram_ros/scripts/telegram_ros_bridge diff --git a/setup.py b/telegram_ros/setup.py similarity index 100% rename from setup.py rename to telegram_ros/setup.py diff --git a/src/telegram_ros/__init__.py b/telegram_ros/src/telegram_ros/__init__.py similarity index 100% rename from src/telegram_ros/__init__.py rename to telegram_ros/src/telegram_ros/__init__.py diff --git a/src/telegram_ros/bridge.py b/telegram_ros/src/telegram_ros/bridge.py similarity index 76% rename from src/telegram_ros/bridge.py rename to telegram_ros/src/telegram_ros/bridge.py index dbb161b..92d3243 100644 --- a/src/telegram_ros/bridge.py +++ b/telegram_ros/src/telegram_ros/bridge.py @@ -1,3 +1,5 @@ +import asyncio + import functools import cv2 @@ -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" @@ -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 @@ -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) @@ -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) @@ -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. @@ -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 @@ -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. @@ -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. @@ -196,19 +198,19 @@ 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. @@ -216,7 +218,8 @@ def _telegram_photo_callback(self, update: Update, _: CallbackContext): :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) @@ -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. @@ -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 @@ -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( diff --git a/test/test_import.py b/telegram_ros/test/test_import.py similarity index 100% rename from test/test_import.py rename to telegram_ros/test/test_import.py diff --git a/CMakeLists.txt b/telegram_ros_msgs/CMakeLists.txt similarity index 57% rename from CMakeLists.txt rename to telegram_ros_msgs/CMakeLists.txt index ba67bd2..9bc9e6b 100644 --- a/CMakeLists.txt +++ b/telegram_ros_msgs/CMakeLists.txt @@ -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 @@ -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() diff --git a/msg/Options.msg b/telegram_ros_msgs/msg/Options.msg similarity index 100% rename from msg/Options.msg rename to telegram_ros_msgs/msg/Options.msg diff --git a/telegram_ros_msgs/package.xml b/telegram_ros_msgs/package.xml new file mode 100644 index 0000000..12844dc --- /dev/null +++ b/telegram_ros_msgs/package.xml @@ -0,0 +1,22 @@ + + + + telegram_ros_msgs + 0.0.0 + The telegram_ros_msgs package + + Rein Appeldoorn + + MIT + + catkin + + message_generation + + message_runtime + + message_runtime + +