起因是Telegram的中文搜索完全不能用,想通过bot对消息进行记录然后再查询,顺便加上了一个发帖提醒功能。

使用的是python-telegram-bot,还是挺简单挺方便的。

简单说明

主要用到了接受消息的Dispatcher类。
以下是官方文档的介绍:

The Updater class continuously fetches new updates from telegram and passes them on to the Dispatcher class. If you create an Updater object, it will create a Dispatcher for you and link them together with a Queue. You can then register handlers of different types in the Dispatcher, which will sort the updates fetched by the Updater according to the handlers you registered, and deliver them to a callback function that you defined.
from telegram.ext import Updater
updater = Updater(token='TOKEN')
dispatcher = updater.dispatcher

记录消息

想要读取消息只要给dispatcher添加处理消息的handler,这里使用的是处理正常消息的MessageHandler,此外还有处理命令的CommandHandler等。
Filter.text过滤文本信息,store_message是处理的函数,传入botupdate这两个参数。
update包含了这次更新的所有内容,主要用到update.message,包括chat_idmessage_id、用户信息、消息内容等,见Telegram api文档bot用于机器人的操作如发送/回复消息,群组操作、修改profile等,和正常用户的权限差不多,这里只读取消息所以没用到。

dispatcher.add_handler(MessageHandler(Filters.text, store_message))
def store_message(bot, update):
    update.message...
    '保存到数据库'

查询消息

最早想的是通过命令加搜索内容来查询,但是这样就需要解决列表和分页问题;后来发现了InlineMode这种方便的东西。

Command和InlineKeyboardMarkup

通过带参数的命令查询数据库并发送一条带有按钮操作的消息,这坨代码有点瞎眼。
首先要将结果构建一个InlineKeyboardButton组成的二维数组(列表),代表行列的按钮,还需要在下面加上翻页按钮。每个按钮除了显示的内容,还带有一个callback_data,被点击时会传给回调函数用来处理按钮点击。
InlineKeyboardMarkup包装,最后通过bot.search_message来发送带有按钮的消息。

dispatcher.add_handler(CommandHandler('search', search_message, pass_args=True))
def search_message(bot, update, args):
    keyword = args[0]
    page = int(args[1]) if len(args) > 1 else 1
    messages = 获取查询结果
    # result button list
    button_list = [
        InlineKeyboardButton(
            '{} | {} | {}'.format(message['text'][:12] + '...' if len(message['text']) > 12 else message['text'],
                                  message['user'], message['time'].strftime("%Y-%m-%d")),
            callback_data='/locate {}'.format(message['id'])) for message in messages
    ]
    prev_button = InlineKeyboardButton(...)
    next_button = InlineKeyboardButton(...)
    pager = ...
    reply_markup = InlineKeyboardMarkup(build_menu(button_list, n_cols=1, footer_buttons=pager))
 
    bot.send_message(chat_id=...,text='...', reply_markup=reply_markup)

下面是按钮的回调函数,这里的update.callback_query就是按钮上带的callback_data,解析它得到消息的ID,再从数据库读取这条消息发出来。
如果是翻页的话需要再调用search_message函数查询,这里有个坑,传过去的query是不带chat_id的,所以还是要写死,而不能动态获取。

dispatcher.add_handler(CallbackQueryHandler(locate_message))

def locate_message(bot, update):
    query = update.callback_query
    args = query.data.split(' ')[1:]
    # change page
    if query.data.startswith('/search'):
        search_message(bot, query, args=args)
    # locate message
    elif query.data.startswith('/locate'):
        msg_id = int(args[0])
        target_message = 根据msg_id从数据库获取这条消息
        if target_message:
            text = 'At: {}/nContent: "{}"'.format(target_message['time'], target_message['text'])
        else:
            text = 'Database Error'
        bot.send_message(chat_id=config.GROUP_ID, reply_to_message_id=msg_id,
                         text=text, disable_notification=True)

InlineMode

上面的方法虽然达到了目标,但是有很多问题。首先搜索的过程太过麻烦不易操作,如果要翻页产生大量信息会影响正常聊天,尽管可以采用定时撤回的方式,但还是有一段时间的滞留;其次,这种搜索方式是不隐蔽的,这个过程中每个人都能够操作,就会很混乱,没有限制的话对后台也是有影响的。

后面找到了InlineMode这个东西,可以在@机器人的时候弹出一个可点击的列表,通过点击某一条,可以以自己的身份发送一条消息出去。
起初发现这条消息也会被机器人获取而记录,而且不能回复也就不能定位到历史消息,结合之前的方法又想到了可以发送命令来定位(命令不会被存储),这次就不需要很多东西,只要一个定位命令加上ID就行了。

每条消息是一个InlineQueryResultArticle,包含标题、描述以及点击会发出去的消息文本input_message_content,把命令放在input_message_content里面,这样点击之后会发出定位消息的命令,触发机器人回复消息。

handler = InlineQueryHandler(inline_caps)
def inline_caps(bot, update):
    query = update.inline_query.query
    处理query获得关键词和页码
    # 结果列表
    results = [InlineQueryResultArticle(
        id='info',
        title='Total:{}. Page {} of {}'.format(count, page, math.ceil(count / SEARCH_PAGE_SIZE)),
        input_message_content=InputTextMessageContent('/help')
    )]
    for message in messages:
        results.append(
            InlineQueryResultArticle(
                id=message['id'],
                title='{}'.format(message['text'][:100]),
                description=message['time'].strftime("%Y-%m-%d").ljust(40) + message['user'],
                input_message_content=InputTextMessageContent('/locate {}'.format(message['id']))
            )
        )
    bot.answer_inline_query(update.inline_query.id, results)

发帖提醒

这个是通过updater.job_queue设置定时任务,如果有贴子更新就发送一条消息,这个比较简单就不说了。

job.run_repeating(one_monitor, interval=60, first=0)
def one_monitor(bot, job):
    for post in get_update_post():
        bot.send_message(...)

总结

总的来说,Telegram的API限制很少,只要脑洞大开就可以做出很多有意思的东西。当然也要防止滥用影响正常聊天,限制使用频率,也可以加上一些自动撤回之类的功能(管理员的话最好把用户命令也撤回)。

参考

https://www.zchen.info/archives/deploying-telegram-bot-with-webhook.html
https://github.com/python-telegram-bot/python-telegram-bot/wiki/Code-snippets#build-a-menu-with-buttons
https://github.com/python-telegram-bot/python-telegram-bot/wiki/Types-Of-Handlers
https://stackoverflow.com/questions/45532078/remove-group-messages-with-python-telegram-bot
https://core.telegram.org/bots/api/#replykeyboardmarkup
https://python-telegram-bot.readthedocs.io/en/latest/telegram.inlinekeyboardbutton.html
https://stackoverflow.com/questions/41195822/multiple-callback-query-handlers
https://python-telegram-bot.readthedocs.io/en/stable/telegram.ext.callbackqueryhandler.html
https://github.com/python-telegram-bot/python-telegram-bot/wiki/Exception-Handling
https://github.com/python-telegram-bot/python-telegram-bot/wiki/Webhooks