先日多言語Webニュースアプリを作りました。 通勤電車で当日のニュースをチェックしながら、外国語を勉強できるので、まあまあ使いやすかったです。

実はこのWebを開発する1年ほど前に、一度Telegram botで多言語ニュースチャットボットを開発していました。今日はそれについて紹介したいと思います。

https://github.com/aibazhang/multitrue-bot

事前準備

Telegram bot

Telegramは日本ではあまり人気がないですが、Telegramのアカウントを作るだけでAPIキーを取得できるので、ざくっとボット作りたい場合は使い勝手がけっこういいです。また、良さげのSDKもあります↓。そういう意味ではSlackも同じですが、Telegramは感覚的にLineに近いので手軽さで勝っていると思います。

https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples

ニュースの取得

ニュースの取得方法は先日開発したWebアプリと同じくNewsAPI利用して取得しています。なぜNewsAPIを採用したかについては半年前の記事で詳細を説明したので、割愛します。

実装

https://core.telegram.org/ に参照してボットの初期設定が完了したら、実装に入ります。 フローは以下となります。

  1. /startでニュースボットを起動
  2. 国・地域を選ぶ
  3. ニュースのジャンルを選ぶ
  4. 終了あるいは2.に戻る

全体フロー

SDKが提供してくれた下記3つのhandlerを利用しています。

  • ConversationHandler: 先ほど設計したフローに基づいてhandlerを定義する。
  • CommandHandler: コマンドによって発火される。今回は「入り口 (entry point)」として使う 
  • CallbackQueryHandler: 他のhandlerの返り値によって発火される
def main():
    updater = Updater(
        token=json.load(open(KEY_PATH / "keys.json", "r"))["telegram_key"],
        use_context=True,
    )

    dispatcher = updater.dispatcher
    country_pattern = "^us|jp|cn|tw|kr|gb$"
    headlines_pattern = "^us|jp|cn|tw|kr|gb business|entertainment|general|health|science|sports|technology|$"

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states={
            "CATEGORY": [CallbackQueryHandler(select_category, pattern=country_pattern)],
            "HEADLINES": [CallbackQueryHandler(get_news, pattern=headlines_pattern)],
            "START OVER OR NOT": [
                CallbackQueryHandler(start_over, pattern="^start over$"),
                CallbackQueryHandler(end, pattern="^end$"),
            ],
        },
        fallbacks=[CommandHandler("start", start)],
    )

    dispatcher.add_handler(conv_handler)

    updater.start_polling()
    updater.idle()

メニュー

続いてはcallback関数を見ていきましょう。start関数はwelcomeメッセージをユーザに送って、ユーザに国・地域を選んでもらいます。その後、select_category関数でニュースのジャンルを選んでもらいます。

引数contextupdate

  • context.user_data: ボットを利用してユーザの情報を取得する
  • context.bot: ボット自体を操作する(今回はメッセージを送るだけ)のに使う
  • update: メッセージを更新するのに使う
def start(update, context):
    user = update.message.from_user
    logger.info("User {} started the conversation.".format(user))
    for i, v in vars(user).items():
        context.user_data[i] = v

    welcome_message = (
        "Hello, {}\n"
        "This is JC News bot🗞️🤖\n\n"
        "You can get Top News Headlines for a Country and a Category from here. \n\n".format(user.first_name)
    )
    print(context)
    keyborad = [
        [
            InlineKeyboardButton("🇺🇸", callback_data="us"),
            InlineKeyboardButton("🇯🇵", callback_data="jp"),
            InlineKeyboardButton("🇹🇼", callback_data="tw"),
        ],
        [
            InlineKeyboardButton("🇰🇷", callback_data="kr"),
            InlineKeyboardButton("🇬🇧", callback_data="gb"),
            InlineKeyboardButton("🇨🇳", callback_data="cn"),
        ],
    ]

    reply_markup = InlineKeyboardMarkup(keyborad)
    context.bot.send_message(chat_id=update.effective_chat.id, text=welcome_message)
    update.message.reply_text("Please Choose a Country🤖", reply_markup=reply_markup)

    return "CATEGORY"
    
def select_category(update, context):
    logger.info("User data from context {}".format(context.user_data))
    logger.info("Chat data from context {}".format(context.chat_data))
    logger.info("Bot data from context {}".format(context.bot_data))

    query = update.callback_query
    query.answer()
    country = query.data
    keyborad = [
        [
            InlineKeyboardButton("👩🏼‍💻Technology", callback_data=country + " technology"),
            InlineKeyboardButton("🧑‍💼Business", callback_data=country + " business"),
        ],
        [
            InlineKeyboardButton("👨🏻‍🎤Entertainment", callback_data=country + " entertainment"),
            InlineKeyboardButton("👩🏻‍⚕️Health", callback_data=country + " health"),
        ],
        [
            InlineKeyboardButton("👨🏿‍🔬Science", callback_data=country + " science"),
            InlineKeyboardButton("🏋🏼‍♂️Sports", callback_data=country + " sports"),
        ],
        [InlineKeyboardButton("🌎General", callback_data=country + " general")],
    ]
    reply_markup = InlineKeyboardMarkup(keyborad)
    query.edit_message_text(text="Please Choose a Category🤖", reply_markup=reply_markup)

    return "HEADLINES"

ニュースを取得

最後は最もコアとなるニュースを取得するロジックです。countrycategoryによってNewAPIから最新ホットラインニュースを取得しフォーマットした後、ユーザにメッセージを送ります。 具体的な詳細は説明しませんが、NewsAPICollectorというクラスでNewAPIをラッピングしています。

https://github.com/aibazhang/multitrue-bot/blob/main/src/news/collector.py

countrycategoryはcallback_dataは上記のcallback関数から取得しています。Slack Appのpayloadと似たような感じです。

def get_news(update, context):
    query = update.callback_query
    query.answer()
    country, category = query.data.split(" ")

    nac = NewsAPICollector(country=country, category=category, page_size=10, print_format="telebot")
    nac.collcet_news()
    news_list = nac.news_list

    news_list = news_list[:5] if len(news_list) > 5 else news_list

    context.bot.send_message(
        chat_id=update.effective_chat.id,
        text="Top {} latest news of [{}] [{}] for you🤖".format(len(news_list), country.upper(), category.upper()),
    )

    for news in news_list:
        context.bot.send_message(chat_id=update.effective_chat.id, text=news)

    keyboard = [
        [
            InlineKeyboardButton("Let's do it again!", callback_data="start over"),
            InlineKeyboardButton("I've had enough...", callback_data="end"),
        ]
    ]

    reply_markup = InlineKeyboardMarkup(keyboard)
    context.bot.send_message(
        chat_id=update.effective_chat.id,
        text="Do you want to start over?🤖",
        reply_markup=reply_markup,
    )

    return "START OVER OR NOT"

デモはリポジトリのREADME.mdのgifから確認できます。

https://github.com/aibazhang/multitrue-bot/

最後に

Telegram bot経由でモバイル開発経験がゼロの人(筆者)もニュースチャットボットを作りました。もちろんニュースだけではなく、定期的に株価や不動産価格を取得したり、英単語帳を作って復習のリマインドしたりするようなWebアプリあるいはモバイルアプリを開発するほどでもないミニアプリに適しているのではないでしょうか?