通过对字符串的解析,执行第一个符合指定规则的回调函数。
基本逻辑参考telegrambot的各个handler,
包括了MessageHandler、CommandHandler和ConversationHandler。
此外,对CommandHandler增加了多级指令的支持。
-
构造输入字符串:
Update(message=Message(1, datetime.now(), 'some user defined string')) -
回调函数定义:
def callback(update: Update, context: CallbackContext)通常无返回值,只有与
ConversationHandler使用时返回int。-
通过
Update.message.text: str获取输入字符串 -
使用
CommandHandler时,通过context.args: List[str]获取指令对应的参数 -
使用
MessageHandler时,通过context.match: re.Match、context.matches: List[re.Match]获取正则filter的匹配结果
-
-
支持的filters过滤器(主要用于
MessageHandler):属性 说明 ALL任意内容 TEXT非空字符串 Text(Iterable[str])包含指定子字符串 Regex(pattern: Union[str, Pattern[str]])匹配正则表达式 可以通过位运算符 (
&for and,|for or,~for not) 连接多个filter。
处理普通消息,通过设置filter过滤消息字符串。
用法示例:
from datetime import datetime
from command_handler import Application, Update, Message, ContextTypes, User, Chat, filters
from command_handler import MessageHandler
def gender(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('input msg:', update.message.text)
print('input gender is:', context.matches[0].group(1))
user = User(0, 'user1')
chat = Chat(0, username=user.username)
app = Application()
app.add_handler(MessageHandler(filters.Text(['I am', "I'm"]) | filters.Regex('(boy|girl)$'), gender))
update = Update(0, Message(0, datetime.now(), chat, user, 'I am a boy'))
app.process_update(update)
# input msg: I am a boy
# input gender is: boy
update2 = Update(1, Message(1, datetime.now(), chat, user, 'I am a girl'))
app.process_update(update2)
# input msg: I am a girl
# input gender is: girl输入字符串作为指令处理,如果指令匹配,后续的部分视为参数列表,可通过回调函数的context.args获取。注:双引号(不含)括起来的部分不会被分割。
支持链式连接,以处理多级指令,如/git remote add <...> <...>
参数说明:
command: str | Collection[str]- 待识别的指令,需要明确指定是否以/开头,大小写不敏感,可以是列表。命名规则为:除开头的/外,其余为1-32位小写英文字母/数字/下划线_/短横线-,如果为空字符串则匹配任意内容。不能与sub_command_handlers同时使用。sub_command_handlers: List[CommandHandler]- 下一级的需要匹配的指令,开头可以不包含'/',可以继续嵌套次级CommandHandler。不能与callback同时使用。default: function (optional)- 默认回调函数。仅与sub_command_handlers同时使用,当所有的次级指令都不匹配时,执行该函数。从不匹配位置起的用户输入都将视为参数包含在context.args内。(注意有多个handler时default的设置是否符合要求,可以考虑额外设置一个handler兜底,或者分成多个group)
用法示例: 一级指令:
from datetime import datetime
from command_handler import Application, Update, Message, ContextTypes
from command_handler import CommandHandler
def set_gender(update: Update, context: ContextTypes.DEFAULT_TYPE):
print(f'You are a {context.args[0]}.')
one_level_handler = CommandHandler('/set-gender', set_gender)
app = Application()
app.add_handler(one_level_handler)
msg = Update(message=Message(1, datetime.now(), text='/set-gender boy'))
app.process_update(msg)
# You are a boy.多级指令:
from datetime import datetime
from command_handler import Application, Update, Message, ContextTypes, User, Chat
from command_handler import CommandHandler
def git_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('function: git_help')
print('args:', str(context.args))
def git_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('function: git_add')
print('args:', str(context.args))
def git_remote_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('function: git_remote_help')
print('args:', str(context.args))
def git_remote_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('function: git_remote_add')
print('args:', str(context.args))
multilevel_handler = CommandHandler(
'/git',
sub_command_handlers=[
CommandHandler('add', git_add),
CommandHandler(
'remote',
sub_command_handlers=[
CommandHandler('add', git_remote_add)
],
default=git_remote_help
)
],
default=git_help
)
user = User(0, 'user1')
chat = Chat(0, username=user.username) # 注意chat id在一场对话中不能变
app = Application()
app.add_handler(multilevel_handler)
update = Update(0, Message(0, datetime.now(), chat, user, '/git add file.py'))
app.process_update(update)
# function: git_add
# args: ['file.py']
update2 = Update(1, Message(1, datetime.now(), chat, user, '/git remote add https://www.gihub.com/zhimengsub/command_handler.git'))
app.process_update(update2)
# function: git_remote_add
# args: ['https://www.gihub.com/zhimengsub/command_handler.git']
update3 = Update(2, Message(2, datetime.now(), chat, user, '/git not supported'))
app.process_update(update3)
# function: git_help
# args: ['not', 'supported']
update4 = Update(3, Message(3, datetime.now(), chat, user, '/git remote also not supported'))
app.process_update(update4)
# function: git_remote_help
# args: ['also', 'not', 'supported']可用的参数:
- entry_points
- states
- fallbacks
- allow_reentry
- per_chat
- per_user
- name
- map_to_parent
使用方法与官方文档基本一致,可参考conversationbot。
此外,增加了通过context.current_state获取当前所在状态。
用法示例: (注意chat id在一场对话中不能变)
#!/usr/bin/env python
import logging
from datetime import datetime
from command_handler import Update, User, Chat, Message, filters
from command_handler import (
Application,
CommandHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
)
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
)
logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4)
def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Starts the conversation and asks the user about their gender."""
print(
"Hi! My name is Professor Bot. I will hold a conversation with you. "
"Send /cancel to stop talking to me.\n\n"
"Are you a boy or a girl?",
)
return GENDER
def gender(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the selected gender and asks for a photo."""
user = update.message.from_user
logger.info("Gender of %s: %s", user.username, update.message.text)
print(
"I see! Please send me a photo of yourself, "
"so I know what you look like, or send /skip if you don't want to.",
)
return PHOTO
def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the photo and asks for a location."""
user = update.message.from_user
logger.info("User %s did not send a photo.", user.username)
print(
"I bet you look great! Now, send me your location please, or send /skip."
)
return LOCATION
def skip_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the location and asks for info about the user."""
user = update.message.from_user
logger.info("User %s did not send a location.", user.username)
print(
"You seem a bit paranoid! At last, tell me something about yourself."
)
return BIO
def bio(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the info about the user and ends the conversation."""
user = update.message.from_user
logger.info("Bio of %s: %s", user.username, update.message.text)
print("Thank you! I hope we can talk again some day.")
return ConversationHandler.END
def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Cancels and ends the conversation."""
user = update.message.from_user
logger.info("User %s canceled the conversation.", user.username)
print(
"Bye! I hope we can talk again some day."
)
return ConversationHandler.END
def resend(update: Update, context: ContextTypes.DEFAULT_TYPE):
print('wrong input at state', context.current_state, 'try again!')
def main() -> None:
"""Run the bot."""
# Create the Application and pass it your bot's token.
application = Application()
# Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO
conv_handler = ConversationHandler(
entry_points=[CommandHandler("/start", start)],
states={
GENDER: [MessageHandler(filters.Regex("^(Boy|Girl|Other)$"), gender)],
PHOTO: [CommandHandler("/skip", skip_photo)],
LOCATION: [
CommandHandler("/skip", skip_location),
],
BIO: [MessageHandler(filters.TEXT, bio)],
},
fallbacks=[CommandHandler("/cancel", cancel), MessageHandler(filters.TEXT, resend)],
)
application.add_handler(conv_handler)
# Run the bot until the user presses Ctrl-C
user = User(0, 'user1')
chat = Chat(0, username=user.username) # 注意chat id在一场对话中不能变
msgs = [
'/start',
'bad input',
'Boy',
'/skip',
'/skip',
'some thing'
]
for i, msg in enumerate(msgs):
update = Update(i, Message(i, datetime.now(), chat, user, msg))
application.process_update(update)
if __name__ == "__main__":
main()
# 2023-06-11 23:31:49,656 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state None
# 2023-06-11 23:31:49,656 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state 0
# 2023-06-11 23:31:49,656 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state 0
# 2023-06-11 23:31:49,656 - __main__ - INFO - Gender of user1: Boy
# 2023-06-11 23:31:49,656 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state 1
# 2023-06-11 23:31:49,656 - __main__ - INFO - User user1 did not send a photo.
# 2023-06-11 23:31:49,657 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state 2
# 2023-06-11 23:31:49,657 - __main__ - INFO - User user1 did not send a location.
# 2023-06-11 23:31:49,657 - commandConversationHandler - DEBUG - Selecting conversation (0, 0) with state 3
# 2023-06-11 23:31:49,657 - __main__ - INFO - Bio of user1: some thing
# Hi! My name is Professor Bot. I will hold a conversation with you. Send /cancel to stop talking to me.
#
# Are you a boy or a girl?
# wrong input at state 0 try again!
# I see! Please send me a photo of yourself, so I know what you look like, or send /skip if you don't want to.
# I bet you look great! Now, send me your location please, or send /skip.
# You seem a bit paranoid! At last, tell me something about yourself.
# Thank you! I hope we can talk again some day.安装依赖:pip install apscheduler
用法与JobQueue一致,但基于BackgroundScheduler,通过context.job_queue获取。
用法示例:(app必须一直运行)
import logging
from datetime import datetime
from command_handler import Application, Update, CommandHandler, ContextTypes, Message, User, Chat
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# Define a few command handlers. These usually take the two arguments update and
# context.
# Best practice would be to replace context with an underscore,
# since context is an unused local variable.
# This being an example and not having context present confusing beginners,
# we decided to have it present as context.
def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Sends explanation on how to use the bot."""
print("Hi! Use /set <seconds> to set a timer")
def alarm(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send the alarm message."""
job = context.job
print(f"Beep! {job.data} seconds are over!")
def remove_job_if_exists(name: str, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Remove job with given name. Returns whether job was removed."""
current_jobs = context.job_queue.get_jobs_by_name(name)
if not current_jobs:
return False
for job in current_jobs:
job.schedule_removal()
return True
def set_timer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Add a job to the queue."""
chat_id = 0
try:
# args[0] should contain the time for the timer in seconds
due = float(context.args[0])
if due < 0:
print("Sorry we can not go back to future!")
return
job_removed = remove_job_if_exists(str(chat_id), context)
context.job_queue.run_once(alarm, due, chat_id=chat_id, name=str(chat_id), data=due)
text = "Timer successfully set!"
if job_removed:
text += " Old one was removed."
print(text)
except (IndexError, ValueError):
print("Usage: /set <seconds>")
def unset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Remove the job if the user changed their mind."""
chat_id = 0
job_removed = remove_job_if_exists(str(chat_id), context)
text = "Timer successfully cancelled!" if job_removed else "You have no active timer."
print(text)
def main() -> None:
"""Run bot."""
# Create the Application and pass it your bot's token.
application = Application()
# on different commands - answer in Telegram
application.add_handler(CommandHandler(["/start", "/help"], start))
application.add_handler(CommandHandler("/set", set_timer))
application.add_handler(CommandHandler("/unset", unset))
application.start()
user = User(0, 'user1')
chat = Chat(0, username=user.username)
update = Update(0, Message(0, datetime.now(), chat, user, '/set 5'))
application.process_update(update)
if __name__ == "__main__":
main()
while True:
pass使用StringArgConverter可以初始化参数的类型、默认值、转换函数等,并在给定真实参数时解析。
具体说明见stringargconverter.py
使用示例:
import datetime
from command_handler import StringArgConverter
def cast_ints(value: str) -> tuple[int, ...]:
"""value: single int, or int separated by ','"""
return tuple(map(int, value.replace(' ', '').split(',')))
def cast_time(value: str) -> datetime.time:
# format: HHMMSS or HHMM
try:
# raise ValueError for wrong format
return datetime.datetime.strptime(value, '%H%M').time()
except ValueError:
# raise ValueError for wrong format
return datetime.datetime.strptime(value, '%H%M%S').time()
usage = StringArgConverter(
'/sub daily <channel> <program> <ids>'
'[excludeProgram] [detail] [checkTime] [days] [startTime] - 添加每日定时检查任务',
# required args
channel=(str,),
program=(str,),
# required args with custom cast function
ids=(list[int], cast_ints),
# positional args (need default value)
excludeProgram=(str, None),
detail=(str, '*'),
# positional args with default value function evaluated at parsing, and custom cast function
checkTime=(datetime.time, lambda: datetime.datetime.now().time(), cast_time),
# positional args with default value and custom cast function
days=(tuple[int],
tuple(range(0, 6+1)),
cast_ints),
startTime=(datetime.time, None, cast_time),
)
args = ['channel_name', 'program_name', '1,2,3', '[再]', 'days=3,4,5']
if not usage.check_arg_len(args):
print(usage.usage)
# Usage: /sub daily <channel> <program> <ids>[excludeProgram] [detail] [checkTime] [days] [startTime] - 添加每日定时检查任务
else:
try:
channel, program, ids, \
excludeProgram, detail, checkTime, days, startTime = usage.parse_args(args)
except (SyntaxError, TypeError, ValueError) as e:
print('参数错误!')TODO async version (JobQueue change to AsyncScheduler)
TODO error handling