python-trueconf-bot 1.1.9__tar.gz → 1.1.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/PKG-INFO +1 -1
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/index.md +5 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/filters.md +82 -0
- python_trueconf_bot-1.1.10/docs/hooks/llms_txt.py +537 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/index.md +4 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/filters.md +86 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/mkdocs.yml +9 -1
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/python_trueconf_bot.egg-info/PKG-INFO +1 -1
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/python_trueconf_bot.egg-info/SOURCES.txt +1 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/_version.py +3 -3
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/send_file_response.py +1 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/.github/workflows/docs.yml +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/.github/workflows/release-test.yml +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/.github/workflows/release.yml +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/.gitignore +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/LICENSE +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/README-PyPI.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/README-ru.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/README.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/assets/head_en.png +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/assets/head_ru.png +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/assets/logo.svg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/examples/echo_bot.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/examples/multibot.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/features.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/img/head.png +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/img/router_scheme_en.svg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/enums.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/files.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/getting-started.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/parse-messages.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/learn/types.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Bot.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Dispatcher.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Enums.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Responses.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Router.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/Types.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/reference/index.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/en/release_notes.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/img/chatbot.svg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/img/favicon.ico +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/img/logo.svg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/overrides/main.html +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/examples/echo_bot.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/examples/multibot.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/features.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/img/head.png +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/img/router_scheme_ru.svg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/enums.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/files.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/getting-started.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/parse-messages.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/learn/types.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/ru/release_notes.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/docs/stylesheets/extra.css +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/README-ru.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/README.md +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/add_big_list_users_to_channel.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/filters.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/report_bot.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/update_token.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/examples/webhook.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/pyproject.toml +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/python_trueconf_bot.egg-info/dependency_links.txt +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/python_trueconf_bot.egg-info/requires.txt +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/python_trueconf_bot.egg-info/top_level.txt +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/requirements-docs.txt +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/requirements.txt +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/setup.cfg +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/client/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/client/bot.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/client/context_controller.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/client/session.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/dispatcher/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/dispatcher/dispatcher.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/dispatcher/router.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/aouth_error.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/chat_participant_role.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/chat_type.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/envelope_author_type.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/file_ready_state.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/incoming_update_method.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/message_type.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/parse_mode.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/survey_type.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/enums/update_type.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/exceptions.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/base.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/command.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/instance_of.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/filters/method.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/loggers.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/add_participant_to_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/auth.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/base.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/change_participant_role.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/create_channel.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/create_favorites_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/create_group_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/create_p2p_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/edit_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/edit_survey.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/forward_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_chat_by_id.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_chat_history.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_chat_participants.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_chats.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_file_info.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_message_by_id.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/get_user_display_name.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/has_chat_participant.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/remove_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/remove_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/remove_participant_from_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/send_file.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/send_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/send_survey.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/subscribe_file_progress.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/unsubscribe_file_progress.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/methods/upload_file.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/py.typed +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/author_box.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/chat_participant.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/attachment.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/base.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/chat_created.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/document.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/forward_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/photo.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/remove_participant.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/sticker.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/survey.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/text.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/content/video.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/input_file.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/last_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/parser.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/added_chat_participant.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/changed_participant_role.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/created_channel.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/created_favorites_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/created_group_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/created_personal_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/edited_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/removed_chat.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/removed_chat_participant.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/removed_message.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/requests/uploading_progress.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/add_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/api_error.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/auth_response_payload.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/change_participant_role_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/create_channel_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/create_favorites_chat_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/create_group_chat_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/create_p2p_chat_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/edit_message_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/edit_survey_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/forward_message_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_chat_by_id_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_chat_history_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_chat_participants_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_chats_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_file_info_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_message_by_id_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/get_user_display_name_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/has_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/remove_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/remove_chat_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/remove_message_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/send_message_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/send_survey_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/subscribe_file_progress_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/unsubscribe_file_progress_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/responses/upload_file_response.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/types/update.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/utils/__init__.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/utils/generate_secret_for_survey.py +0 -0
- {python_trueconf_bot-1.1.9 → python_trueconf_bot-1.1.10}/trueconf/utils/token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-trueconf-bot
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.10
|
|
4
4
|
Summary: Lightweight and powerful framework for the TrueConf Server Chatbot API
|
|
5
5
|
Author-email: TrueConf LLC <info@trueconf.com>, Anton Baadzhi <baadzhianton@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause-Clear
|
|
@@ -41,3 +41,8 @@ pip install {{product_name}}
|
|
|
41
41
|
| JSON → Python | Pydantic models | Mashumaro dataclasses |
|
|
42
42
|
| Transport | HTTPS + long polling / webhook | Asynchronous WebSocket |
|
|
43
43
|
| Working with files | `bot.get_file(...)` + `bot.download_file(...)` | `message.photo.download()`, `message.document.download()`, `bot.download_file_by_id(...)` |
|
|
44
|
+
|
|
45
|
+
## LLM Files
|
|
46
|
+
|
|
47
|
+
- [llms.txt](https://trueconf.github.io/python-trueconf-bot/master/llms.txt)
|
|
48
|
+
- [llms-full.txt](https://trueconf.github.io/python-trueconf-bot/master/llms-full.txt)
|
|
@@ -215,8 +215,90 @@ F.text.upper().in_({"FOO", "BAR"}) # message.text.upper() in {"FOO", "BAR"}
|
|
|
215
215
|
F.text.len() == 5 # len(message.text) == 5
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
## Building Custom Filters
|
|
219
|
+
|
|
220
|
+
You can build custom filters to fine-tune your event-handling logic. This allows you to move condition checks—such as verifying whether a user exists in the database or checking access permissions—into a separate, reusable component.
|
|
221
|
+
|
|
222
|
+
**Implementation requirements:**
|
|
223
|
+
|
|
224
|
+
* **Class-based structure:** A custom filter must be implemented as a class. This makes it possible to preserve state, such as filter settings, when the filter is initialized.
|
|
225
|
+
* **`__call__` method:** The class must implement a `__call__` method. Without it, the class instance will not be considered callable, and the filter will not work.
|
|
226
|
+
* **Return value:** The `__call__` method must return `True` if the event matches the filter conditions, or `False` if the event should be ignored by the handler.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from typing import Any
|
|
230
|
+
|
|
231
|
+
class MyFilter:
|
|
232
|
+
"""
|
|
233
|
+
This filter checks whether a user belongs
|
|
234
|
+
to the specified group via the server API.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, target_group: str):
|
|
238
|
+
# Store the required group when creating the instance
|
|
239
|
+
self.target_group = target_group
|
|
240
|
+
|
|
241
|
+
async def __call__(self, event: Any) -> bool:
|
|
242
|
+
user = getattr(event, "from_user", None)
|
|
243
|
+
if not user:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
user_id = user.id
|
|
247
|
+
# Perform the check, for example via an API call
|
|
248
|
+
user_info: dict = await server_api.get_user(user_id)
|
|
249
|
+
groups = user_info.get("groups", [])
|
|
250
|
+
|
|
251
|
+
# Make sure to return True or False
|
|
252
|
+
return any(group.get("id") == self.target_group for group in groups)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Once the filter class is defined, you can use it in handler decorators exactly the same way as the built-in **Command** or **F** filters. When registering a handler, simply pass an instance of your class as an argument.
|
|
256
|
+
|
|
257
|
+
```python hl_lines="8"
|
|
258
|
+
from aiogram import Router, Message
|
|
259
|
+
# Import your filter class
|
|
260
|
+
from my_filters import MyFilter
|
|
261
|
+
|
|
262
|
+
router = Router()
|
|
263
|
+
|
|
264
|
+
# Attach the filter in the same way as Command or F
|
|
265
|
+
@router.message(MyFilter(target_group="0032"))
|
|
266
|
+
async def my_handler(message: Message):
|
|
267
|
+
await message.answer("Access granted: you belong to the required group!")
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
When a message is received, the bot will use the class instance, call its `__call__` method, and, if it returns `True`, execute the handler code.
|
|
271
|
+
|
|
218
272
|
## Combining Filters
|
|
219
273
|
|
|
274
|
+
In **python-trueconf-bot**, you can combine multiple filters within a single handler. By default, when filters are separated by commas, they use **AND** logic—the handler will run only if **all** filters return `True`.
|
|
275
|
+
|
|
276
|
+
**Command and a custom filter**
|
|
277
|
+
|
|
278
|
+
You can restrict access to a specific command by using your own filter. The handler will run only if the message is the specified command **and** your filter returns `True`.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
# Triggers if the /admin command is received
|
|
282
|
+
# AND the user belongs to group "0032"
|
|
283
|
+
@r.message(Command("admin"), InGroupFilter(target_group="0032"))
|
|
284
|
+
async def admin_panel(message: Message):
|
|
285
|
+
await message.answer("Welcome to the admin panel!")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**MagicFilter (F) and a custom filter**
|
|
289
|
+
|
|
290
|
+
Combine message-content checks with your own access rules. For example, you can react to a specific word in the text only for users from certain groups:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
# Triggers if the text contains "help"
|
|
294
|
+
# AND the user belongs to the "support" group
|
|
295
|
+
@r.message(F.text.contains("help"), InGroupFilter(target_group="0003"))
|
|
296
|
+
async def support_handler(message: Message):
|
|
297
|
+
await message.answer("The support team has been notified of your request.")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Combining Command Arguments and MagicFilter
|
|
301
|
+
|
|
220
302
|
Combining **Command** and **MagicFilter** is a common and recommended practice:
|
|
221
303
|
**Command** parses the command and creates a **CommandObject**, while `magic`
|
|
222
304
|
allows you to impose additional conditions on `args` or other parts of the
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from html import unescape
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
8
|
+
from urllib.parse import urljoin
|
|
9
|
+
|
|
10
|
+
from mkdocs.utils import log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ALLOWED_PATTERNS = [
|
|
14
|
+
r"^index\.html$",
|
|
15
|
+
r"^(features|release_notes)/index\.html$",
|
|
16
|
+
r"^learn/.*/index\.html$",
|
|
17
|
+
r"^reference/.*/index\.html$",
|
|
18
|
+
r"^examples/.*/index\.html$",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
EXCLUDED = {
|
|
22
|
+
"404.html",
|
|
23
|
+
"sitemap.xml",
|
|
24
|
+
"objects.inv",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
SECTION_LABELS = {
|
|
28
|
+
"learn": "Learn",
|
|
29
|
+
"reference": "Reference",
|
|
30
|
+
"examples": "Examples",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SECTION_ORDER = {
|
|
34
|
+
"root": 0,
|
|
35
|
+
"learn": 1,
|
|
36
|
+
"reference": 2,
|
|
37
|
+
"examples": 3,
|
|
38
|
+
"other": 4,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BLOCK_TAGS = ("p", "div", "section", "article", "blockquote")
|
|
42
|
+
LIST_TAGS = ("ul", "ol")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def should_include_page(path: str) -> bool:
|
|
46
|
+
"""Return True when the rendered file should be included in llms outputs."""
|
|
47
|
+
normalized = path.replace("\\", "/").lstrip("/")
|
|
48
|
+
|
|
49
|
+
if normalized in EXCLUDED:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if not normalized.endswith(".html"):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
if normalized.startswith("ru/") or "/ru/" in normalized:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return any(re.match(pattern, normalized) for pattern in ALLOWED_PATTERNS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_main_content(html: str) -> str:
|
|
62
|
+
"""Extract the main Material content area from rendered HTML."""
|
|
63
|
+
patterns = [
|
|
64
|
+
r'<article class="md-content__inner md-typeset">(.*?)</article>',
|
|
65
|
+
r"<article[^>]*class=\"[^\"]*md-content__inner[^\"]*md-typeset[^\"]*\"[^>]*>(.*?)</article>",
|
|
66
|
+
r"<main[^>]*>(.*?)</main>",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for pattern in patterns:
|
|
70
|
+
match = re.search(pattern, html, re.DOTALL | re.IGNORECASE)
|
|
71
|
+
if match:
|
|
72
|
+
return match.group(1)
|
|
73
|
+
|
|
74
|
+
return html
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def strip_tags(value: str) -> str:
|
|
78
|
+
"""Remove HTML tags and normalize whitespace in a short fragment."""
|
|
79
|
+
value = re.sub(r"<[^>]+>", " ", value)
|
|
80
|
+
value = unescape(value)
|
|
81
|
+
value = re.sub(r"\s+", " ", value)
|
|
82
|
+
return value.strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalize_text(text: str) -> str:
|
|
86
|
+
"""Normalize whitespace while preserving paragraph-like spacing."""
|
|
87
|
+
text = text.replace("\r", "")
|
|
88
|
+
text = re.sub(r"\n[ \t]+", "\n", text)
|
|
89
|
+
text = re.sub(r"[ \t]+\n", "\n", text)
|
|
90
|
+
text = re.sub(r"[ \t]{2,}", " ", text)
|
|
91
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
92
|
+
return text.strip()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def convert_table_to_markdown(table_html: str) -> str:
|
|
96
|
+
"""Convert a basic HTML table to Markdown."""
|
|
97
|
+
rows = re.findall(r"<tr[^>]*>(.*?)</tr>", table_html, re.DOTALL | re.IGNORECASE)
|
|
98
|
+
parsed_rows = []
|
|
99
|
+
|
|
100
|
+
for row in rows:
|
|
101
|
+
cells = re.findall(r"<t[hd][^>]*>(.*?)</t[hd]>", row, re.DOTALL | re.IGNORECASE)
|
|
102
|
+
cleaned = [normalize_text(html_to_markdown(cell)) for cell in cells]
|
|
103
|
+
if cleaned:
|
|
104
|
+
parsed_rows.append(cleaned)
|
|
105
|
+
|
|
106
|
+
if not parsed_rows:
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
column_count = max(len(row) for row in parsed_rows)
|
|
110
|
+
padded_rows = [row + [""] * (column_count - len(row)) for row in parsed_rows]
|
|
111
|
+
|
|
112
|
+
header = padded_rows[0]
|
|
113
|
+
separator = ["---"] * column_count
|
|
114
|
+
body = padded_rows[1:]
|
|
115
|
+
|
|
116
|
+
lines = [
|
|
117
|
+
"| " + " | ".join(header) + " |",
|
|
118
|
+
"| " + " | ".join(separator) + " |",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for row in body:
|
|
122
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
123
|
+
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def convert_details_to_markdown(details_html: str) -> str:
|
|
128
|
+
"""Convert details/summary blocks into visible Markdown sections."""
|
|
129
|
+
summary_match = re.search(
|
|
130
|
+
r"<summary[^>]*>(.*?)</summary>",
|
|
131
|
+
details_html,
|
|
132
|
+
re.DOTALL | re.IGNORECASE,
|
|
133
|
+
)
|
|
134
|
+
summary = strip_tags(summary_match.group(1)) if summary_match else "Details"
|
|
135
|
+
|
|
136
|
+
content = re.sub(
|
|
137
|
+
r"<summary[^>]*>.*?</summary>",
|
|
138
|
+
"",
|
|
139
|
+
details_html,
|
|
140
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Avoid recursive processing when details blocks are nested.
|
|
144
|
+
content = re.sub(
|
|
145
|
+
r"<summary[^>]*>(.*?)</summary>",
|
|
146
|
+
lambda match: f"<h4>{strip_tags(match.group(1))}</h4>",
|
|
147
|
+
content,
|
|
148
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
149
|
+
)
|
|
150
|
+
content = re.sub(r"</?details[^>]*>", "", content, flags=re.IGNORECASE)
|
|
151
|
+
|
|
152
|
+
content_md = normalize_text(html_to_markdown(content))
|
|
153
|
+
|
|
154
|
+
if content_md:
|
|
155
|
+
return f"\n\n### {summary}\n\n{content_md}\n\n"
|
|
156
|
+
|
|
157
|
+
return f"\n\n### {summary}\n\n"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def html_to_markdown(html: str) -> str:
|
|
161
|
+
"""Convert rendered HTML into readable Markdown for llms outputs."""
|
|
162
|
+
if not html:
|
|
163
|
+
return ""
|
|
164
|
+
|
|
165
|
+
text = html
|
|
166
|
+
|
|
167
|
+
text = re.sub(r"<script\b[^>]*>.*?</script>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
168
|
+
text = re.sub(r"<style\b[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
169
|
+
text = re.sub(
|
|
170
|
+
r"<a[^>]*class=\"[^\"]*headerlink[^\"]*\"[^>]*>.*?</a>",
|
|
171
|
+
"",
|
|
172
|
+
text,
|
|
173
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
174
|
+
)
|
|
175
|
+
text = re.sub(
|
|
176
|
+
r"<span[^>]*class=\"[^\"]*gp[^\"]*\"[^>]*>.*?</span>",
|
|
177
|
+
"",
|
|
178
|
+
text,
|
|
179
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
180
|
+
)
|
|
181
|
+
text = re.sub(r"</?span[^>]*>", "", text, flags=re.IGNORECASE)
|
|
182
|
+
|
|
183
|
+
text = re.sub(
|
|
184
|
+
r"<details\b[^>]*>.*?</details>",
|
|
185
|
+
lambda match: convert_details_to_markdown(match.group(0)),
|
|
186
|
+
text,
|
|
187
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
text = re.sub(
|
|
191
|
+
r"<table\b[^>]*>.*?</table>",
|
|
192
|
+
lambda match: "\n\n" + convert_table_to_markdown(match.group(0)) + "\n\n",
|
|
193
|
+
text,
|
|
194
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def replace_pre(match: re.Match) -> str:
|
|
198
|
+
attrs = match.group(1) or ""
|
|
199
|
+
code = match.group(2)
|
|
200
|
+
lang_match = re.search(r"language-([a-zA-Z0-9_+\-.]+)", attrs)
|
|
201
|
+
language = lang_match.group(1) if lang_match else ""
|
|
202
|
+
code_text = unescape(re.sub(r"<[^>]+>", "", code)).strip("\n")
|
|
203
|
+
return f"\n\n```{language}\n{code_text}\n```\n\n"
|
|
204
|
+
|
|
205
|
+
text = re.sub(
|
|
206
|
+
r"<pre[^>]*><code([^>]*)>(.*?)</code></pre>",
|
|
207
|
+
replace_pre,
|
|
208
|
+
text,
|
|
209
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
text = re.sub(
|
|
213
|
+
r"<code[^>]*>(.*?)</code>",
|
|
214
|
+
lambda match: f"`{strip_tags(match.group(1))}`",
|
|
215
|
+
text,
|
|
216
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def replace_anchor(match: re.Match) -> str:
|
|
220
|
+
attrs = match.group(1) or ""
|
|
221
|
+
content = strip_tags(match.group(2))
|
|
222
|
+
href_match = re.search(r'href=["\']([^"\']+)["\']', attrs, re.IGNORECASE)
|
|
223
|
+
href = href_match.group(1).strip() if href_match else ""
|
|
224
|
+
|
|
225
|
+
if not content:
|
|
226
|
+
return ""
|
|
227
|
+
|
|
228
|
+
if href and not href.startswith("#"):
|
|
229
|
+
return f"[{content}]({href})"
|
|
230
|
+
|
|
231
|
+
return content
|
|
232
|
+
|
|
233
|
+
text = re.sub(
|
|
234
|
+
r"<a([^>]*)>(.*?)</a>",
|
|
235
|
+
replace_anchor,
|
|
236
|
+
text,
|
|
237
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
for level in range(6, 0, -1):
|
|
241
|
+
pattern = rf"<h{level}[^>]*>(.*?)</h{level}>"
|
|
242
|
+
text = re.sub(
|
|
243
|
+
pattern,
|
|
244
|
+
lambda match, lvl=level: f"\n\n{'#' * lvl} {strip_tags(match.group(1))}\n\n",
|
|
245
|
+
text,
|
|
246
|
+
flags=re.DOTALL | re.IGNORECASE,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
|
|
250
|
+
text = re.sub(r"</li>", "\n", text, flags=re.IGNORECASE)
|
|
251
|
+
text = re.sub(r"<li[^>]*>", "\n- ", text, flags=re.IGNORECASE)
|
|
252
|
+
|
|
253
|
+
for tag in BLOCK_TAGS:
|
|
254
|
+
text = re.sub(rf"<{tag}[^>]*>", "\n\n", text, flags=re.IGNORECASE)
|
|
255
|
+
text = re.sub(rf"</{tag}>", "\n\n", text, flags=re.IGNORECASE)
|
|
256
|
+
|
|
257
|
+
for tag in LIST_TAGS:
|
|
258
|
+
text = re.sub(rf"<{tag}[^>]*>", "\n", text, flags=re.IGNORECASE)
|
|
259
|
+
text = re.sub(rf"</{tag}>", "\n", text, flags=re.IGNORECASE)
|
|
260
|
+
|
|
261
|
+
text = re.sub(r"<[^>]+>", "", text)
|
|
262
|
+
text = unescape(text)
|
|
263
|
+
text = normalize_text(text)
|
|
264
|
+
|
|
265
|
+
lines = []
|
|
266
|
+
for line in text.split("\n"):
|
|
267
|
+
stripped = line.strip()
|
|
268
|
+
if stripped.startswith("-"):
|
|
269
|
+
stripped = re.sub(r"^-\s*", "- ", stripped)
|
|
270
|
+
lines.append(stripped if stripped else "")
|
|
271
|
+
|
|
272
|
+
text = "\n".join(lines)
|
|
273
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
274
|
+
return text.strip()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_version() -> str:
|
|
278
|
+
"""Resolve the project version from package metadata or git tags."""
|
|
279
|
+
try:
|
|
280
|
+
return package_version("python-trueconf-bot")
|
|
281
|
+
except PackageNotFoundError:
|
|
282
|
+
pass
|
|
283
|
+
except Exception as exc: # pragma: no cover
|
|
284
|
+
log.debug(f"llms-txt: Failed to read package metadata version: {exc}")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
return (
|
|
288
|
+
subprocess.check_output(
|
|
289
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
290
|
+
stderr=subprocess.DEVNULL,
|
|
291
|
+
text=True,
|
|
292
|
+
)
|
|
293
|
+
.strip()
|
|
294
|
+
)
|
|
295
|
+
except Exception as exc: # pragma: no cover
|
|
296
|
+
log.debug(f"llms-txt: Failed to read git version: {exc}")
|
|
297
|
+
|
|
298
|
+
return "dev"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_page_title(html: str, fallback: str) -> str:
|
|
302
|
+
"""Extract a human-readable page title from rendered HTML."""
|
|
303
|
+
h1_match = re.search(r"<h1[^>]*>(.*?)</h1>", html, re.DOTALL | re.IGNORECASE)
|
|
304
|
+
if h1_match:
|
|
305
|
+
title = strip_tags(h1_match.group(1))
|
|
306
|
+
if title:
|
|
307
|
+
return title
|
|
308
|
+
|
|
309
|
+
title_match = re.search(r"<title[^>]*>(.*?)</title>", html, re.DOTALL | re.IGNORECASE)
|
|
310
|
+
if title_match:
|
|
311
|
+
raw_title = strip_tags(title_match.group(1))
|
|
312
|
+
title = re.split(r"\s[-|–—]\s", raw_title)[0].strip()
|
|
313
|
+
if title:
|
|
314
|
+
return title
|
|
315
|
+
|
|
316
|
+
slug = fallback.replace("index.html", "").strip("/")
|
|
317
|
+
if not slug:
|
|
318
|
+
return "Home"
|
|
319
|
+
|
|
320
|
+
return slug.split("/")[-1].replace("-", " ").replace("_", " ").title()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def normalize_doc_path(doc_path: str) -> str:
|
|
324
|
+
"""Normalize a nav doc path into the corresponding rendered html path."""
|
|
325
|
+
path = doc_path.replace("\\", "/").strip("/")
|
|
326
|
+
|
|
327
|
+
if path.endswith(".md"):
|
|
328
|
+
path = path[:-3]
|
|
329
|
+
|
|
330
|
+
if not path or path == ".":
|
|
331
|
+
return "index.html"
|
|
332
|
+
|
|
333
|
+
if path.endswith("/index"):
|
|
334
|
+
return f"{path}.html"
|
|
335
|
+
|
|
336
|
+
return f"{path}/index.html"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def flatten_nav(nav_items) -> list[str]:
|
|
340
|
+
"""Flatten MkDocs nav config into ordered rendered html paths."""
|
|
341
|
+
ordered_paths: list[str] = []
|
|
342
|
+
|
|
343
|
+
def traverse(item) -> None:
|
|
344
|
+
if isinstance(item, list):
|
|
345
|
+
for nested in item:
|
|
346
|
+
traverse(nested)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
if isinstance(item, dict):
|
|
350
|
+
for value in item.values():
|
|
351
|
+
traverse(value)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
if isinstance(item, str):
|
|
355
|
+
ordered_paths.append(normalize_doc_path(item))
|
|
356
|
+
|
|
357
|
+
traverse(nav_items or [])
|
|
358
|
+
return ordered_paths
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_section_key(path: str) -> str:
|
|
362
|
+
normalized = path.replace("\\", "/")
|
|
363
|
+
if normalized == "index.html" or normalized.startswith(("features/", "release_notes/")):
|
|
364
|
+
return "root"
|
|
365
|
+
if normalized.startswith("learn/"):
|
|
366
|
+
return "learn"
|
|
367
|
+
if normalized.startswith("reference/"):
|
|
368
|
+
return "reference"
|
|
369
|
+
if normalized.startswith("examples/"):
|
|
370
|
+
return "examples"
|
|
371
|
+
return "other"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def path_sort_key(path: str, nav_positions: dict[str, int]):
|
|
375
|
+
section = get_section_key(path)
|
|
376
|
+
nav_position = nav_positions.get(path, 10**6)
|
|
377
|
+
return (
|
|
378
|
+
SECTION_ORDER.get(section, 99),
|
|
379
|
+
nav_position,
|
|
380
|
+
path.count("/"),
|
|
381
|
+
path.lower(),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def build_url(base_url: str, rel_path: str) -> str:
|
|
386
|
+
"""Build a public URL for a rendered html file."""
|
|
387
|
+
normalized = rel_path.replace("\\", "/").lstrip("/")
|
|
388
|
+
if normalized == "index.html":
|
|
389
|
+
return base_url
|
|
390
|
+
|
|
391
|
+
if normalized.endswith("/index.html"):
|
|
392
|
+
normalized = normalized[: -len("index.html")]
|
|
393
|
+
|
|
394
|
+
return urljoin(base_url + "/", normalized)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def make_indent(level: int) -> str:
|
|
398
|
+
return " " * max(level, 0)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def add_index_entry(index_lines: list[str], title: str, url: str, level: int = 0) -> None:
|
|
402
|
+
index_lines.append(f"{make_indent(level)}- [{title}]({url})")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_index_lines(page_entries: list[dict]) -> list[str]:
|
|
406
|
+
"""Build the hierarchical INDEX section."""
|
|
407
|
+
index_lines = ["## INDEX"]
|
|
408
|
+
|
|
409
|
+
home_entry = next((entry for entry in page_entries if entry["path"] == "index.html"), None)
|
|
410
|
+
features_entry = next(
|
|
411
|
+
(entry for entry in page_entries if entry["path"] == "features/index.html"),
|
|
412
|
+
None,
|
|
413
|
+
)
|
|
414
|
+
release_notes_entry = next(
|
|
415
|
+
(entry for entry in page_entries if entry["path"] == "release_notes/index.html"),
|
|
416
|
+
None,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if home_entry:
|
|
420
|
+
add_index_entry(index_lines, home_entry["title"], home_entry["url"], level=0)
|
|
421
|
+
if features_entry:
|
|
422
|
+
add_index_entry(index_lines, features_entry["title"], features_entry["url"], level=0)
|
|
423
|
+
|
|
424
|
+
grouped_sections = {"learn": [], "reference": [], "examples": []}
|
|
425
|
+
for entry in page_entries:
|
|
426
|
+
section = get_section_key(entry["path"])
|
|
427
|
+
if section in grouped_sections:
|
|
428
|
+
grouped_sections[section].append(entry)
|
|
429
|
+
|
|
430
|
+
for section_key in ("learn", "reference", "examples"):
|
|
431
|
+
entries = grouped_sections[section_key]
|
|
432
|
+
if not entries:
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
index_lines.append(f"- [{SECTION_LABELS[section_key]}](#{section_key})")
|
|
436
|
+
for entry in entries:
|
|
437
|
+
add_index_entry(index_lines, entry["title"], entry["url"], level=1)
|
|
438
|
+
|
|
439
|
+
if release_notes_entry:
|
|
440
|
+
add_index_entry(index_lines, release_notes_entry["title"], release_notes_entry["url"], level=0)
|
|
441
|
+
|
|
442
|
+
return index_lines
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def write_lines(path: str, lines: list[str]) -> None:
|
|
446
|
+
"""Write text lines using UTF-8 encoding."""
|
|
447
|
+
with open(path, "w", encoding="utf-8") as file_obj:
|
|
448
|
+
file_obj.write("\n".join(lines).strip() + "\n")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def on_post_build(config):
|
|
452
|
+
"""Generate llms.txt and llms-full.txt from rendered HTML files."""
|
|
453
|
+
site_dir = config["site_dir"]
|
|
454
|
+
base_url = (config.get("site_url") or "http://127.0.0.1:8000").rstrip("/")
|
|
455
|
+
nav_paths = flatten_nav(config.get("nav", []))
|
|
456
|
+
nav_positions = {path: idx for idx, path in enumerate(nav_paths)}
|
|
457
|
+
|
|
458
|
+
discovered_paths = []
|
|
459
|
+
for root, _, filenames in os.walk(site_dir):
|
|
460
|
+
for filename in filenames:
|
|
461
|
+
rel_path = os.path.relpath(os.path.join(root, filename), site_dir)
|
|
462
|
+
rel_path = rel_path.replace("\\", "/")
|
|
463
|
+
if should_include_page(rel_path):
|
|
464
|
+
discovered_paths.append(rel_path)
|
|
465
|
+
|
|
466
|
+
sorted_paths = sorted(set(discovered_paths), key=lambda path: path_sort_key(path, nav_positions))
|
|
467
|
+
|
|
468
|
+
page_entries = []
|
|
469
|
+
for rel_path in sorted_paths:
|
|
470
|
+
abs_path = os.path.join(site_dir, rel_path)
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
with open(abs_path, "r", encoding="utf-8") as file_obj:
|
|
474
|
+
html = file_obj.read()
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
log.warning(f"llms-txt: Failed to read rendered HTML {rel_path}: {exc}")
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
main_html = extract_main_content(html)
|
|
480
|
+
markdown = html_to_markdown(main_html)
|
|
481
|
+
title = get_page_title(html, rel_path)
|
|
482
|
+
url = build_url(base_url, rel_path)
|
|
483
|
+
|
|
484
|
+
page_entries.append(
|
|
485
|
+
{
|
|
486
|
+
"path": rel_path,
|
|
487
|
+
"title": title,
|
|
488
|
+
"url": url,
|
|
489
|
+
"content": markdown,
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
index_lines = build_index_lines(page_entries)
|
|
494
|
+
generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
495
|
+
version = get_version()
|
|
496
|
+
|
|
497
|
+
llms_lines = [
|
|
498
|
+
"# Python TrueConf Bot SDK",
|
|
499
|
+
*index_lines,
|
|
500
|
+
"",
|
|
501
|
+
"## CONTENT",
|
|
502
|
+
"See `llms-full.txt` for the complete rendered documentation.",
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
full_lines = [
|
|
506
|
+
"# Python TrueConf Bot SDK",
|
|
507
|
+
"## METADATA",
|
|
508
|
+
f"- **Version:** {version}",
|
|
509
|
+
f"- **Generated:** {generated_at}",
|
|
510
|
+
f"- **URL:** {base_url}/",
|
|
511
|
+
"",
|
|
512
|
+
"---",
|
|
513
|
+
"",
|
|
514
|
+
*index_lines,
|
|
515
|
+
"",
|
|
516
|
+
"---",
|
|
517
|
+
"",
|
|
518
|
+
"## CONTENT",
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
for entry in page_entries:
|
|
522
|
+
full_lines.extend(
|
|
523
|
+
[
|
|
524
|
+
"",
|
|
525
|
+
f"# DOCUMENT: {entry['title']}",
|
|
526
|
+
f"**Source:** {entry['url']}",
|
|
527
|
+
"",
|
|
528
|
+
entry["content"] or "_No content extracted._",
|
|
529
|
+
"",
|
|
530
|
+
"---",
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
write_lines(os.path.join(site_dir, "llms.txt"), llms_lines)
|
|
535
|
+
write_lines(os.path.join(site_dir, "llms-full.txt"), full_lines)
|
|
536
|
+
|
|
537
|
+
log.info("llms-txt: Generated llms.txt and llms-full.txt from rendered HTML")
|
|
@@ -35,4 +35,8 @@ pip install {{product_name}}
|
|
|
35
35
|
- TrueConf API становится «человеческим»: вместо JSON-словарей вы работаете с Python-классами.
|
|
36
36
|
- Вы получаете удобный инструмент для интеграции ботов в корпоративные чаты TrueConf.
|
|
37
37
|
|
|
38
|
+
## Документация для ИИ (LLM)
|
|
39
|
+
|
|
40
|
+
- [llms.txt](https://trueconf.github.io/python-trueconf-bot/master/llms.txt)
|
|
41
|
+
- [llms-full.txt](https://trueconf.github.io/python-trueconf-bot/master/llms-full.txt)
|
|
38
42
|
|