An easy to use an (hopefully useful) captcha solution for pyTelegramBotAPI

Overview

pyTelegramBotCAPTCHA

An easy to use and (hopefully useful) image CAPTCHA soltion for pyTelegramBotAPI.

PyPi Package Version Supported Python versions


Installation:

pip install pyTelegramBotCAPTCHA

Do not forget to update the package from time to time by calling
pip install pyTelegramBotCAPTCHA --upgrade


Description:

Do you have problems with userbots that spam your groups or add your group members to other chats? Then this package can help you to protect your groups and members! It's very easy to integrate into your existing bot and also easy to customize the CAPTCHA image with your own fonts.
You can also choose between digits and hexdigits for your CAPTCHA generation.
Note: You should have basic knowledge about the pyTelegramBotAPI
Example1 Example2


Writing a CAPTCHA bot:

Import TeleBot and the CapchaManager:

from telebot import TeleBot
from pyTelegramBotCAPTCHA import CaptchaManager

Initialize the bot and the captcha_manager:

CaptchaManager requires the user_id of your TeleBot instance! You get it with bot.get_me().id
You can add the following optional parameters:

  • default_language (str) the default language to use if not set in captcha_manager.send_random_captcha(...). Default is "en". Currently supported "en", "ru" and "de"
  • default_timeout (float) the default timeout to use if not set in captcha_manager.send_random_captcha(...). Default is None but we will use a default_timeout of 90 seconds for our CAPTCHAs.
  • fonts (list) the fonts to use instead of the builtin ones (must be a list of .ttf file paths). You can choose as many fonts as you like, but keep in mind that all the fonts are loaded into your memory, so use a lot but not to many.
bot = TeleBot("TOKEN")
captcha_manager = CaptchaManager(bot.get_me().id, default_timeout=90)

Note: Make sure to actually replace TOKEN with your own API token


Add a message handler for new chat members:

We need a message handler to restrict the new member and sending a CAPTCHA to solve when a new user joins the group.
captcha_manager.restrict_chat_member() requires your TeleBot instance, the chat_id and the user_id. It disables all permissions of a chat member.
captcha_manager.send_random_captcha() requires your TeleBot instance, the Chat object and the User object. It sends a new CAPTCHA in the chat.
You can add the following optional parameters:

  • language (str) the language to use for this CAPTCHA
  • add_noise (bool) add noise to the CAPTCHA image
  • only_digits (bool) only use ditgits instead of hexdigits for the CAPTCHA code
  • timeout (float) to set a timeout for the CAPTCHA in seconds.
# Message handler for new chat members
@bot.message_handler(content_types=["new_chat_members"])
def new_member(message):
  # get the new chat members
  for user in message.new_chat_members:

    # Restrict the new chat member
    captcha_manager.restrict_chat_member(bot, message.chat.id, user.id)

    # send random CAPTCHA
    captcha_manager.send_random_captcha(bot, message.chat, user)

Note: Service messages about non-bot users joining the chat will be soon removed from large groups. We recommend using the “chat_member” update as a replacement.


Add a callback query handler:

We need a callback query handler, to handle the users input when he presses a CAPTCHA button.
captcha_manager.update_captcha() requires your TeleBot instance and the CallbackQuery object as parameters.
It automatically returns if callback was not from a CAPTCHA or from the wrong user.
If the wrong user pressed a button he gets an callback query answer denying his input.
If the submit button is pressed the CAPTCHA is automatically checked and your corresponding CAPTCHA handler function is called. The timeout is also canceled if submit is pressed.

# Callback query handler
@bot.callback_query_handler(func=lambda callback:True)
def on_callback(callback):
  # update the CAPTCHA
  captcha_manager.update_captcha(bot, callback)

Add CAPTCHA handler functions:

This works just like you know it from message handlers from the pyTelegramBotAPI.
A Captcha object will be passed to your functions.
The Captcha object has the following attributes:

  • message_id (int) the message id of the CAPTCHA message
  • user (User) the user that must solve the CAPTCHA
  • chat (Chat) the chat
  • users_code (str) the code entered by the user
  • correct_code (str) the correct code to solve the CAPTCHA
  • language (str) the language of the CAPTCHA text
  • created_at (float) the timestemp when the CAPTCHA was created
  • previous_tries (int) the number of tries the user made
  • incorrect_digits (int) the number of digits that dont match
  • solved (bool) has the user solved the CAPTCHA? it does not matter if he solved it correct

Lets add our first CAPTCHA handler that handles correct solved CAPTCHAs. captcha_manager.unrestrict_chat_member() requires your TeleBot instance, the chat_id and the user_id. It removes all restictions of a chat member.
captcha_manager.delete_captcha() requires your TeleBot instance and the Captcha object. It removes the CAPTCHA from the chat and your memory

# Handler for correct solved CAPTCHAs
@captcha_manager.on_captcha_correct
def on_correct(captcha):
  bot.send_message(captcha.chat.id, "Congrats! You solved the CAPTCHA!")
  # We unrestrict the chat member because he solved the CAPTCHA correct.
  captcha_manager.unrestrict_chat_member(bot, captcha.chat.id, captcha.user.id)
  # Delete the CAPTCHA
  captcha_manager.delete_captcha(bot, captcha)

Lets add a handler that handles wrong solved CAPTCHAs.
We use the Captcha attributes incorrect_digits and previous_tries to give the user a second try if only one digit was incorrect.
captcha_manager.refresh_captcha() requires your TeleBot instance and the Captcha object. It generates a new code image.
You can add the following optional parameters:

  • add_noise (bool) add noise to the CAPTCHA image
  • only_digits (bool) only use ditgits instead of hexdigits for the CAPTCHA code
  • timeout (float) set new timeout because the previous is canceled. If not set it will captcha_manager.default_timeout (if set).
# Handler for wrong solved CAPTCHAs
@captcha_manager.on_captcha_not_correct
def on_not_correct(captcha):
  # Check if only one dicit was incorrect and the user only did one try
  if (captcha.incorrect_digits == 1 and captcha.previous_tries < 2):
    # Refresh the CAPTCHA
    captcha_manager.refresh_captcha(bot, captcha)
  else:
    # We ban the chat member because he failed solving the CAPTCHA.
    bot.kick_chat_member(captcha.chat.id, captcha.user.id)
    bot.send_message(captcha.chat.id, f"{captcha.user.first_name} failed solving the CAPTCHA and was banned!")
    # Delete the CAPTCHA
    captcha_manager.delete_captcha(bot, captcha)

Now lets add a handler that handles timed out CAPTCHAs

# Handler for timed out CAPTCHAS
@captcha_manager.on_captcha_timeout
def on_timeout(captcha):
  # We ban the chat member because he did not solve the CAPTCHA.
  bot.kick_chat_member(captcha.chat.id, captcha.user.id)
  bot.send_message(captcha.chat.id, f"{captcha.user.first_name} did not solve the CAPTCHA and was banned!")
  # Delete the CAPTCHA
  captcha_manager.delete_captcha(bot, captcha)

The finished CAPTCHA bot

Now we only have to add the line bot.polling()at the end of our script and we have a finished CAPTCHA bot that looks like this:

from telebot import TeleBot
from pyTelegramBotCAPTCHA import CaptchaManager
                                                                    
bot = TeleBot("TOKEN")
captcha_manager = CaptchaManager(bot.get_me().id)

# Message handler for new chat members
@bot.message_handler(content_types=["new_chat_members"])
def new_member(message):
  new_user_id = message.json.get("new_chat_member").get("id")
  new_user = bot.get_chat_member(message.chat.id, new_user_id).user
  captcha_manager.restrict_chat_member(bot, message.chat.id, new_user.id)
  captcha_manager.send_random_captcha(bot, message.chat, new_user)
                                                                    
# Callback query handler
@bot.callback_query_handler(func=lambda callback:True)
def on_callback(callback):
  captcha_manager.update_captcha(bot, callback)
                                                                    
# Handler for correct solved CAPTCHAs
@captcha_manager.on_captcha_correct
def on_correct(captcha):
  bot.send_message(captcha.chat.id, "Congrats! You solved the CAPTCHA!")
  captcha_manager.unrestrict_chat_member(bot, captcha.chat.id, captcha.user.id)
  captcha_manager.delete_captcha(bot, captcha)

# Handler for wrong solved CAPTCHAs
@captcha_manager.on_captcha_not_correct
def on_not_correct(captcha):
  if (captcha.incorrect_digits == 1 and captcha.previous_tries < 2):
    captcha_manager.refresh_captcha(bot, captcha)
  else:
    bot.kick_chat_member(captcha.chat.id, captcha.user.id)
    bot.send_message(captcha.chat.id, f"{captcha.user.first_name} failed solving the CAPTCHA and was banned!")
    captcha_manager.delete_captcha(bot, captcha)
  
# Handler for timed out CAPTCHAS
@captcha_manager.on_captcha_timeout
def on_timeout(captcha):
  bot.kick_chat_member(captcha.chat.id, captcha.user.id)
  bot.send_message(captcha.chat.id, f"{captcha.user.first_name} did not solve the CAPTCHA and was banned!")
  captcha_manager.delete_captcha(bot, captcha)
  
bot.polling()
Comments
  • Issues with CustomLanguage parameters 'your_code' and 'try_again'

    Issues with CustomLanguage parameters 'your_code' and 'try_again'

    Let's take the following setup:

    languages = CustomLanguage() languages.text = "text" languages.try_again = "again" languages.wrong_user = "wrong" languages.too_short = "short" languages.your_code = "code"

    'try_again' is actually not shown when it should. You keep seeing 'text' on both a wrong attempt and a manual user refresh.

    'your_code' is also not shown when it should. You always see the default "Your Code:", and 'your_code' does show up but only on a wrong attempt or a manual user refresh (it is not visible on the initial captcha). However, "Your Code:" is still visible up to this point, so you actually see two lines, one time the default one, and one time the new custom one.

    bug 
    opened by tinderboxmedia 5
  • Inconsistency: A captcha reload on an incorrect submit has default values

    Inconsistency: A captcha reload on an incorrect submit has default values

    If an individual fails to solve the captcha and if another attempt is allowed, the captcha will refresh but it will override variables such as timeout, digits_only and add_noice. This feels like an inconsistency in the logic as a refresh should give a new captcha but with all the user declared variables and values. If the function is declared to not add_noice or to be digits_only, a refresh should not change this behaviour, unless declared otherwise.

    To fix this inconsistency the captcha refresh should either follow the user declared variables and values, or one should be able to allow this override to happen or not.

    enhancement 
    opened by tinderboxmedia 5
  • No timeout anymore when user has made a wrong attempt

    No timeout anymore when user has made a wrong attempt

    There seems to be an error where there is no timeout event anymore when a user made a wrong attempt, it also seems to not work anymore when a user manually refreshed the CAPTCHA. It still happens though when a user is still on the initial CAPTCHA. I tried this with the default timeout of 90, and a custom timeout. Note that max_incorrect_to_auto_reload has been set to 1, so a timeout should still happen.

    bug 
    opened by tinderboxmedia 4
  • Suggestion: Be able to declare the digits and hex-digits used in the captcha outside of the module

    Suggestion: Be able to declare the digits and hex-digits used in the captcha outside of the module

    Currently the only way to change the digits and hex-digits used in the captcha is to change variables inside of the module. The suggestion would be to be able to declare which digits or letters can be used, but still in a set of 10 (the 2 by 5 input markup grid), or a set of 16 (the 4x4 input markup grid).

    The main reason for this suggestion is that some fonts do have characters that could potentially look to similar. For example: 8 and B, or 1 and 7. Be able to declare a custom set of 10 or 16 characters, the captchas could become more user friendly but still bot proof.

    opened by tinderboxmedia 4
  • Suggestion: Be able to declare the length of the captcha

    Suggestion: Be able to declare the length of the captcha

    Currently the length of the captcha showed is defaulted and hardcoded to be 8 characters long. The suggestion would be to be able to declare the length of the captcha, so that it can display less or even more characters that need to be solved.

    enhancement 
    opened by tinderboxmedia 4
  • KeyZenD returns a 404. Fallback to 'Default' might be needed.

    KeyZenD returns a 404. Fallback to 'Default' might be needed.

    Seems like the KeyZenD CAPTCHA can't be found anymore: https://tyt.xyeta.ml/captcha.png

    This page returns an 404, and the CAPTCHA can't be found. Maybe it would be smart to add some sort of fallback that turns the option back to 'default' as currently this would break the entire thing?

    opened by tinderboxmedia 3
  • Issue: Adding 1+ users to the channel will only trigger 1 captcha

    Issue: Adding 1+ users to the channel will only trigger 1 captcha

    After some more stress testing it seems to be the case that if someone invites or adds more than 1 user (the option to mass invite your contacts to a channel for example) to the channel where the bot is active, it will only create 1 captcha. Only one user has to complete the captcha, the other users are already 'in' (so to speak).

    opened by tinderboxmedia 3
  • ERROR: TypeError: 'float' object cannot be interpreted as an integer

    ERROR: TypeError: 'float' object cannot be interpreted as an integer

    I try init a new bot with

    from telebot import TeleBot
    from pyTelegramBotCAPTCHA import CaptchaManager
                                                                        
    bot = TeleBot("MY_TOKEN")
    captcha_manager = CaptchaManager(bot.get_me().id)
    
    # Message handler for new chat members
    @bot.message_handler(content_types=["new_chat_members"])
    def new_member(message):
      for new_user in message.new_chat_members:
        captcha_manager.restrict_chat_member(bot, message.chat.id, new_user.id)
        captcha_manager.send_new_captcha(bot, message.chat, new_user)
                                                                        
    # Callback query handler
    @bot.callback_query_handler(func=lambda callback:True)
    def on_callback(callback):
      captcha_manager.update_captcha(bot, callback)
                                                                        
    # Handler for correct solved CAPTCHAs
    @captcha_manager.on_captcha_correct
    def on_correct(captcha):
      bot.send_message(captcha.chat.id, "Congrats! You solved the CAPTCHA!")
      captcha_manager.unrestrict_chat_member(bot, captcha.chat.id, captcha.user.id)
      captcha_manager.delete_captcha(bot, captcha)
    
    # Handler for wrong solved CAPTCHAs
    @captcha_manager.on_captcha_not_correct
    def on_not_correct(captcha):
      if (captcha.incorrect_digits == 1 and captcha.previous_tries < 2):
        captcha_manager.refresh_captcha(bot, captcha)
      else:
        bot.kick_chat_member(captcha.chat.id, captcha.user.id)
        bot.send_message(captcha.chat.id, f"{captcha.user.first_name} failed solving the CAPTCHA and was banned!")
        captcha_manager.delete_captcha(bot, captcha)
    
    # Handler for timed out CAPTCHAS
    @captcha_manager.on_captcha_timeout
    def on_timeout(captcha):
      bot.kick_chat_member(captcha.chat.id, captcha.user.id)
      bot.send_message(captcha.chat.id, f"{captcha.user.first_name} did not solve the CAPTCHA and was banned!")
      captcha_manager.delete_captcha(bot, captcha)
      
    bot.polling()
    

    And i take a error:

    Traceback (most recent call last):
      File "/Users/olivmath/repo/lunes/bot-telegram-cruzeiro/olivmath.py", line 43, in <module>
        bot.polling()
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/telebot/__init__.py", line 660, in polling
        self.__threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/telebot/__init__.py", line 722, in __threaded_polling
        raise e
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/telebot/__init__.py", line 682, in __threaded_polling
        self.worker_pool.raise_exceptions()
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/telebot/util.py", line 135, in raise_exceptions
        raise self.exception_info
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/telebot/util.py", line 87, in run
        task(*args, **kwargs)
      File "/Users/olivmath/repo/lunes/bot-telegram-cruzeiro/olivmath.py", line 12, in new_member
        captcha_manager.send_new_captcha(bot, message.chat, new_user)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/pyTelegramBotCAPTCHA/telebot_captcha.py", line 553, in send_new_captcha
        captcha = Captcha(bot, chat, user, options)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/pyTelegramBotCAPTCHA/telebot_captcha.py", line 345, in __init__
        self.correct_code, self.image = _random_codeimage(self.options)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/pyTelegramBotCAPTCHA/telebot_captcha.py", line 865, in _random_codeimage
        image = image.generate_image(code)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/captcha/image.py", line 228, in generate_image
        im = self.create_captcha_image(chars, color, background)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/captcha/image.py", line 212, in create_captcha_image
        mask = im.convert('L').point(table)
      File "/Users/olivmath/Library/Caches/pypoetry/virtualenvs/bot-telegram-cruzeiro-rcJnbm2r-py3.10/lib/python3.10/site-packages/PIL/Image.py", line 1680, in point
        return self._new(self.im.point(lut, mode))
    TypeError: 'float' object cannot be interpreted as an integer
    
    opened by olivmath 2
  • Refresh button

    Refresh button

    Hi there,

    Great work - loving your captcha bot! I had one idea that would be great to implement. A Refresh button in case the captcha is really hard / barley readable.

    Keep it up!

    enhancement good first issue 
    opened by fabston 2
  • Issue: Adding the code_length argument to send_random_captcha will throw a TypeError.

    Issue: Adding the code_length argument to send_random_captcha will throw a TypeError.

    For example, trying initialize the captcha using captcha_manager.send_random_captcha(bot, message.chat, new_user, timeout=60, code_length=4, language="en") will result in TypeError: send_random_captcha() got an unexpected keyword argument 'code_length'

    However, following the documentation this should be possible.

    opened by tinderboxmedia 2
  • Issue: If a captcha is not properly removed after kicking a user, it will throw a KeyError when trying to interact with it again

    Issue: If a captcha is not properly removed after kicking a user, it will throw a KeyError when trying to interact with it again

    While interacting with a captcha button (update_captcha), it should first be checked if that captcha session is still 'available' or not. It not being 'available' can be due to that the captcha message was not deleted properly after kicking a user and that user did not solve it, or that the file that holds that captcha session is somehow removed.

    Right now it will throw a KeyError if the session can't be found, but if we can check if that captcha session is not available anymore, we could just remove the captcha instead (whoever presses it) and clean up.

    opened by tinderboxmedia 2
Releases(1.1.4)
Python Computer Vision Aim Bot for Roblox's Phantom Forces

Python-Phantom-Forces-Aim-Bot Python Computer Vision Aim Bot for Roblox's Phanto

drag0ngam3s 2 Jul 11, 2022
Comparison-of-OCR (KerasOCR, PyTesseract,EasyOCR)

Optical Character Recognition OCR (Optical Character Recognition) is a technology that enables the conversion of document types such as scanned paper

21 Dec 25, 2022
MORAN: A Multi-Object Rectified Attention Network for Scene Text Recognition

MORAN: A Multi-Object Rectified Attention Network for Scene Text Recognition Python 2.7 Python 3.6 MORAN is a network with rectification mechanism for

Canjie Luo 595 Dec 27, 2022
A python script based on opencv and paddleocr, which can automatically pick up tasks, make cookies, and receive rewards in the Destiny 2 Dawning Oven

A python script based on opencv and paddleocr, which can automatically pick up tasks, make cookies, and receive rewards in the Destiny 2 Dawning Oven

1 Dec 22, 2021
Forked from argman/EAST for the ICPR MTWI 2018 CHALLENGE

EAST_ICPR: EAST for ICPR MTWI 2018 CHALLENGE Introduction This is a repository forked from argman/EAST for the ICPR MTWI 2018 CHALLENGE. Origin Reposi

Haozheng Li 157 Aug 23, 2022
Links to awesome OCR projects

Awesome OCR This list contains links to great software tools and libraries and literature related to Optical Character Recognition (OCR). Contribution

Konstantin Baierer 2.2k Jan 02, 2023
DouZero is a reinforcement learning framework for DouDizhu - 斗地主AI

[ICML 2021] DouZero: Mastering DouDizhu with Self-Play Deep Reinforcement Learning | 斗地主AI

Kwai 3.1k Jan 05, 2023
An organized collection of tutorials and projects created for aspriring computer vision students.

A repository created with the purpose of teaching students in BME lab 308A- Hanoi University of Science and Technology

Givralnguyen 5 Nov 24, 2021
Balabobapy - Using artificial intelligence algorithms to continue the text

Balabobapy - Using artificial intelligence algorithms to continue the text

qxtony 1 Feb 04, 2022
Total Text Dataset. It consists of 1555 images with more than 3 different text orientations: Horizontal, Multi-Oriented, and Curved, one of a kind.

Total-Text-Dataset (Official site) Updated on April 29, 2020 (Detection leaderboard is updated - highlighted E2E methods. Thank you shine-lcy.) Update

Chee Seng Chan 671 Dec 27, 2022
a micro OCR network with 0.07mb params.

MicroOCR a micro OCR network with 0.07mb params. Layer (type) Output Shape Param # Conv2d-1 [-1, 64, 8,

william 29 Aug 06, 2022
OCR, Scene-Text-Understanding, Text Recognition

Scene-Text-Understanding Survey [2015-PAMI] Text Detection and Recognition in Imagery: A Survey paper [2014-Front.Comput.Sci] Scene Text Detection and

Alan Tang 354 Dec 12, 2022
ERQA - Edge Restoration Quality Assessment

ERQA - a full-reference quality metric designed to analyze how good image and video restoration methods (SR, deblurring, denoising, etc) are restoring real details.

MSU Video Group 27 Dec 17, 2022
The project is an official implementation of our paper "3D Human Pose Estimation with Spatial and Temporal Transformers".

3D Human Pose Estimation with Spatial and Temporal Transformers This repo is the official implementation for 3D Human Pose Estimation with Spatial and

Ce Zheng 363 Dec 28, 2022
A curated list of papers and resources for scene text detection and recognition

Awesome Scene Text A curated list of papers and resources for scene text detection and recognition The year when a paper was first published, includin

Jan Zdenek 43 Mar 15, 2022
caffe re-implementation of R2CNN: Rotational Region CNN for Orientation Robust Scene Text Detection

R2CNN: Rotational Region CNN for Orientation Robust Scene Text Detection Abstract This is a caffe re-implementation of R2CNN: Rotational Region CNN fo

candler 80 Dec 28, 2021
Hiiii this is the Spanish for Linux and win 10 and in the near future the english version of PortScan my new tool on which you can see what ports are Open only with the IP adress.

PortScanner-by-IIT PortScanner es una herramienta programada en Python3. Como su nombre indica esta herramienta escanea los primeros 150 puertos de re

5 Sep 19, 2022
This can be use to convert text in a file to handwritten text.

TextToHandwriting This can be used to convert text to handwriting. Clone this project or download the code. Run TextToImage.py give the filename of th

Ashutosh Mahapatra 2 Feb 06, 2022
In this project we will be using the live feed coming from the webcam to create a virtual mouse with complete functionalities.

Virtual Mouse Using OpenCV In this project we will be using the live feed coming from the webcam to create a virtual mouse using hand tracking. Projec

Hassan Shahzad 8 Dec 20, 2022
CUTIE (TensorFlow implementation of Convolutional Universal Text Information Extractor)

CUTIE TensorFlow implementation of the paper "CUTIE: Learning to Understand Documents with Convolutional Universal Text Information Extractor." Xiaohu

Zhao,Xiaohui 147 Dec 20, 2022