Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
!tests/**
!CMakeLists.txt
!conanfile.py
!coverage_summary.py
!Makefile
!poetry.lock
!pyproject.toml
4 changes: 4 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
branches:
- master

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
branches:
- master

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand Down Expand Up @@ -50,9 +54,26 @@ jobs:
cache-from: type=gha,scope=test
cache-to: type=gha,mode=max,scope=test
- name: Test tgbot-cpp
run: make docker-test-only
run: |
docker run --name tgbot-cpp-coverage reo7sp/tgbot-cpp-test sh -eu -c '
make test-only
make coverage
'
docker cp tgbot-cpp-coverage:/usr/src/tgbot-cpp/build/Debug/coverage ./coverage
docker rm tgbot-cpp-coverage
- name: Test codegen
run: make docker-test-api-codegen
- name: Upload HTML coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: coverage
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: coverage/coveralls.json
format: coveralls

test-windows:
runs-on: windows-2022
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
project(TgBot LANGUAGES CXX)

option(ENABLE_TESTS "Build tests" OFF)
option(ENABLE_SANITIZERS "Instrument the library and tests with address and undefined behavior sanitizers" OFF)
option(ENABLE_COVERAGE "Instrument the library and tests for code coverage" OFF)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(BUILD_DOCUMENTATION "Build API documentation" OFF)

Expand Down Expand Up @@ -39,6 +41,20 @@ target_compile_features(TgBot PUBLIC cxx_std_20)
target_compile_options(TgBot PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall>
)
if(ENABLE_SANITIZERS)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
message(FATAL_ERROR "ENABLE_SANITIZERS requires GCC or Clang")
endif()
target_compile_options(TgBot PUBLIC -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(TgBot PUBLIC -fsanitize=address,undefined)
endif()
if(ENABLE_COVERAGE)
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC")
endif()
target_compile_options(TgBot PRIVATE -O0 -g --coverage)
target_link_options(TgBot PUBLIC --coverage)
endif()
target_include_directories(TgBot PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
Expand Down
10 changes: 8 additions & 2 deletions Dockerfile_test
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ RUN apt-get -qq update && \
python3-venv >/dev/null && \
rm -rf /var/lib/apt/lists/*

ENV CONAN_BUILD_ARGS="--build=missing:gtest/*"

RUN python3 -m venv /opt/conan && \
/opt/conan/bin/pip install --quiet --disable-pip-version-check --no-cache-dir \
"clang-format==22.1.8" \
"conan==2.31.1" \
"gcovr==8.6" \
"poetry==2.4.1"
ENV PATH="/opt/conan/bin:${PATH}"

WORKDIR /usr/src/tgbot-cpp

COPY conanfile.py Makefile poetry.lock pyproject.toml ./

ENV BUILD_TYPE=Debug
ENV ENABLE_SANITIZERS=ON
ENV ENABLE_COVERAGE=ON
ENV ASAN_OPTIONS=detect_leaks=1:halt_on_error=1
ENV UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1

RUN make dependencies-with-test

COPY include include
Expand All @@ -39,6 +44,7 @@ COPY api_codegen api_codegen
COPY cmake cmake
COPY .clang-format ./
COPY CMakeLists.txt ./
COPY coverage_summary.py ./

RUN make build-with-test && \
make install-only
Expand Down
45 changes: 42 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ CONAN_RECIPE_ARGS ?=
CMAKE_LOG_LEVEL ?= STATUS
CMAKE_INSTALL_MESSAGE ?= ALWAYS
CMAKE_BUILD_ARGS ?=
ENABLE_SANITIZERS ?= OFF
ENABLE_COVERAGE ?= OFF
POETRY_INSTALL_ARGS ?= --no-interaction --quiet
INSTALL_PREFIX ?=
API_METHODS_CPP = src/ApiMethods.cpp
API_METHODS_INC = include/tgbot/ApiMethods.inc.h
API_METHODS_CLANG_FORMAT_CONFIG = api_codegen/clang-format-api-methods.yaml
CPP_FILES = $(shell find include src tests examples -type f \( -name '*.h' -o -name '*.cpp' \) ! -name '*.inc.h' ! -path '$(API_METHODS_CPP)' | sort)
PYTHON_FILES = conanfile.py api_codegen
PYTHON_FILES = conanfile.py coverage_summary.py api_codegen
DOCKER_IMAGE ?= reo7sp/tgbot-cpp
DOCKER_TEST_IMAGE ?= reo7sp/tgbot-cpp-test
DOCKER_PLATFORM ?= linux/amd64
Expand All @@ -23,6 +25,7 @@ EXAMPLES := $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard examples/*/CMakeLis
EXAMPLE_IMAGE ?= tgbot-cpp-example-$(EXAMPLE)
PORT ?= 8080
DOCS_WORKTREE := $(abspath build/gh-pages)
COVERAGE_REPORT_DIR ?= $(BUILD_DIR)/coverage

ifeq ($(OS),Windows_NT)
export CMAKE_GENERATOR ?= Ninja
Expand All @@ -46,6 +49,8 @@ endif
test \
test-only \
test-api-codegen \
coverage \
coverage-python \
install \
install-with-system \
install-only \
Expand Down Expand Up @@ -94,13 +99,17 @@ configure: dependencies
-DCMAKE_TOOLCHAIN_FILE=$(abspath $(BUILD_DIR)/generators/conan_toolchain.cmake) \
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_TESTS=OFF

configure-with-system:
cmake --log-level=$(CMAKE_LOG_LEVEL) -S . -B $(BUILD_DIR) \
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_TESTS=OFF

Expand All @@ -109,6 +118,8 @@ configure-with-test: dependencies-with-test
-DCMAKE_TOOLCHAIN_FILE=$(abspath $(BUILD_DIR)/generators/conan_toolchain.cmake) \
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_TESTS=ON

Expand Down Expand Up @@ -147,6 +158,35 @@ test-only:
test-api-codegen:
poetry run pytest api_codegen/tests

coverage:
cmake -E make_directory $(COVERAGE_REPORT_DIR)
gcovr $(BUILD_DIR) \
--root . \
--filter 'include/tgbot/' \
--filter 'src/' \
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
--exclude-throw-branches \
--exclude-unreachable-branches \
--html-details $(COVERAGE_REPORT_DIR)/coverage.html
gcovr $(BUILD_DIR) \
--root . \
--filter 'include/tgbot/' \
--filter 'src/' \
--exclude 'src/ApiMethods\.cpp' \
--exclude 'src/Types\.cpp' \
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
--exclude-throw-branches \
--exclude-unreachable-branches \
--coveralls $(COVERAGE_REPORT_DIR)/coveralls.json \
--lcov $(COVERAGE_REPORT_DIR)/lcov.info
python3 coverage_summary.py $(COVERAGE_REPORT_DIR)/coveralls.json

coverage-python:
cmake -E make_directory $(BUILD_DIR)
COVERAGE_FILE=$(BUILD_DIR)/.coverage-python poetry run pytest api_codegen/tests \
--cov=api_codegen \
--cov-report=term-missing

install: build
$(MAKE) install-only

Expand Down Expand Up @@ -230,8 +270,7 @@ docker-run-example-webhook: docker-example-image
docker run --rm -it --init --platform=$(DOCKER_PLATFORM) -e TOKEN -e WEBHOOK_URL -p $(PORT):8080 $(EXAMPLE_IMAGE)

docker-compose-run-examples: docker-image
DOCKER_PLATFORM=$(DOCKER_PLATFORM) TGBOT_CPP_IMAGE=$(DOCKER_IMAGE) \
docker compose --env-file env -f docker-compose.test.yaml up --build
DOCKER_PLATFORM=$(DOCKER_PLATFORM) TGBOT_CPP_IMAGE=$(DOCKER_IMAGE) docker compose --env-file env -f docker-compose.test.yaml up --build

docker-compose-stop-examples:
docker compose --env-file env -f docker-compose.test.yaml down
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# tgbot-cpp

[![GitHub contributors](https://img.shields.io/github/contributors/reo7sp/tgbot-cpp.svg)](https://github.com/reo7sp/tgbot-cpp/graphs/contributors)
[![Coverage Status](https://coveralls.io/repos/github/reo7sp/tgbot-cpp/badge.svg?branch=master)](https://coveralls.io/github/reo7sp/tgbot-cpp?branch=master)

C++ library for Telegram bot API.

Expand Down
20 changes: 19 additions & 1 deletion api/codegen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ types:
api:
addStickerToSet:
args_order: [user_id, name, sticker]
supports_attach_references: true
answerCallbackQuery:
args_order: [callback_query_id, text, show_alert, url, cache_time]
answerInlineQuery:
Expand Down Expand Up @@ -80,6 +81,7 @@ api:
provider_token: {declaration_required: true}
createNewStickerSet:
args_order: [user_id, name, title, stickers, sticker_type, needs_repainting]
supports_attach_references: true
args:
sticker_type: {type: "Sticker::Type", default: "Sticker::Type::Regular"}
deleteMyCommands:
Expand All @@ -97,12 +99,16 @@ api:
editMessageMedia:
return_type: std::shared_ptr<Message>
args_order: [media, chat_id, message_id, inline_message_id, reply_markup]
supports_attach_references: true
editMessageReplyMarkup:
return_type: std::shared_ptr<Message>
args_order: [chat_id, message_id, inline_message_id, reply_markup]
editMessageText:
return_type: std::shared_ptr<Message>
args_order: [text, chat_id, message_id, inline_message_id, parse_mode, link_preview_options, reply_markup, entities]
supports_attach_references: true
editStory:
supports_attach_references: true
forwardMessage:
args_order: [chat_id, from_chat_id, message_id, disable_notification, protect_content, message_thread_id]
forwardMessages:
Expand All @@ -123,6 +129,7 @@ api:
args_order: [chat_id, user_id, can_change_info, can_post_messages, can_edit_messages, can_delete_messages, can_invite_users, can_pin_messages, can_promote_members, is_anonymous, can_manage_chat, can_manage_video_chats, can_restrict_members, can_manage_topics, can_post_stories, can_edit_stories, can_delete_stories]
replaceStickerInSet:
args_order: [user_id, name, old_sticker, sticker]
supports_attach_references: true
restrictChatMember:
args_order: [chat_id, user_id, permissions, until_date, use_independent_chat_permissions]
sendAnimation:
Expand All @@ -146,7 +153,12 @@ api:
sendLocation:
args_order: [chat_id, latitude, longitude, live_period, reply_parameters, reply_markup, disable_notification, horizontal_accuracy, heading, proximity_alert_radius, message_thread_id, protect_content, business_connection_id]
sendMediaGroup:
args_order: [chat_id, media, disable_notification, reply_parameters, message_thread_id, protect_content, business_connection_id]
args_order: [chat_id, media, disable_notification, reply_parameters, message_thread_id, protect_content, business_connection_id, allow_paid_broadcast, direct_messages_topic_id, message_effect_id]
supports_attach_references: true
sendPaidMedia:
supports_attach_references: true
sendRichMessage:
supports_attach_references: true
sendMessage:
args_order: [chat_id, text, link_preview_options, reply_parameters, reply_markup, parse_mode, disable_notification, entities, message_thread_id, protect_content, business_connection_id]
sendPhoto:
Expand All @@ -165,11 +177,17 @@ api:
args_order: [chat_id, video_note, reply_parameters, disable_notification, duration, length, thumbnail, reply_markup, message_thread_id, protect_content, business_connection_id]
sendVoice:
args_order: [chat_id, voice, caption, duration, reply_parameters, reply_markup, parse_mode, disable_notification, caption_entities, message_thread_id, protect_content, business_connection_id]
postStory:
supports_attach_references: true
setBusinessAccountProfilePhoto:
supports_attach_references: true
setChatAdministratorCustomTitle:
args_order: [chat_id, user_id, custom_title]
setGameScore:
return_type: std::shared_ptr<Message>
args_order: [user_id, score, force, disable_edit_message, chat_id, message_id, inline_message_id]
setMyProfilePhoto:
supports_attach_references: true
setMessageReaction:
args_order: [chat_id, message_id, reaction, is_big]
setMyCommands:
Expand Down
28 changes: 24 additions & 4 deletions api_codegen/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
CONFIG = yaml.safe_load(DEFAULT_CONFIG.read_text(encoding="utf-8"))
TYPE_CONFIG = CONFIG.get("types", {})
API_CONFIG = CONFIG.get("api", {})
ATTACH_REFERENCES_DESCRIPTION = (
"Files uploaded as named multipart parts. Reference each file from a composite "
"Telegram API argument as attach://<name> and use the same name in InputFileAttachment."
)


def run(schema_path: Path, root: Path) -> None:
Expand Down Expand Up @@ -99,6 +103,7 @@ class _MethodModel:
return_description: str
description: tuple[str, ...]
args: tuple[_ArgModel, ...]
compatibility_args: tuple[_ArgModel, ...]
args_name: str | None


Expand Down Expand Up @@ -364,11 +369,14 @@ def _build_method(self, operation: Schema) -> _MethodModel:
properties = dict(request.get("properties", {}))
for arg_name, arg in multipart.get("properties", {}).items():
properties.setdefault(arg_name, arg)
method_config = self._api_config.get(name, {})
if method_config.get("supports_attach_references"):
properties["attachments"] = {"description": ATTACH_REFERENCES_DESCRIPTION}
properties.update(method_config.get("extra_args", {}))
required = set(request.get("required", [])) | set(multipart.get("required", []))
binary = self._binary_args(operation)
arg_names = self._ordered_arg_names(name, properties, required)
response_type = _CppTypeResolver.api_type(self._response_schema(operation))
method_config = self._api_config.get(name, {})
return_type = method_config.get("return_type", response_type)
args = tuple(
self._build_arg(
Expand All @@ -380,13 +388,15 @@ def _build_method(self, operation: Schema) -> _MethodModel:
)
for arg_name in arg_names
)
compatibility_without = set(method_config.get("compatibility_overload_without", ()))
return _MethodModel(
name=name,
return_type=return_type,
response_type=response_type,
return_description=self._return_description(return_type, response_type),
description=self._comment_lines(operation.get("description", ""), 88),
args=args,
compatibility_args=tuple(arg for arg in args if arg.wire_name not in compatibility_without),
args_name=f"{name[0].upper()}{name[1:]}Args" if args else None,
)

Expand All @@ -411,7 +421,13 @@ def _ordered_arg_names(
),
)
optional_names = [name for name in properties if name not in declaration_required]
optional_names.sort(key=lambda name: (args_order.get(name, len(args_order)), name))
optional_names.sort(
key=lambda name: (
name == "attachments" and method_config.get("supports_attach_references"),
args_order.get(name, len(args_order)),
name,
)
)

return required_names + optional_names

Expand All @@ -424,7 +440,9 @@ def _build_arg(
binary: bool,
) -> _ArgModel:
arg_config = self._api_config.get(method_name, {}).get("args", {}).get(name, {})
override = arg_config.get("type")
method_config = self._api_config.get(method_name, {})
attach_references_arg = name == "attachments" and method_config.get("supports_attach_references")
override = "std::vector<InputFileAttachment>" if attach_references_arg else arg_config.get("type")
if override:
cpp_type = override
elif name in {"chat_id", "from_chat_id"}:
Expand Down Expand Up @@ -499,7 +517,9 @@ def _return_description(return_type: str, response_type: str) -> str:

@staticmethod
def _arg_declaration_type(cpp_type: str) -> str:
if cpp_type == "std::string" or cpp_type.startswith("std::vector<"):
if cpp_type == "std::string":
return "std::string_view"
if cpp_type.startswith("std::vector<"):
return f"const {cpp_type}&"
if cpp_type == "nlohmann::json":
return "const nlohmann::json&"
Expand Down
Loading
Loading