Skip to content

Commit a2324e8

Browse files
committed
Support specify file content in sendMediaGroup
1 parent a8db26f commit a2324e8

42 files changed

Lines changed: 1571 additions & 676 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/test.yml‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,26 @@ jobs:
5050
cache-from: type=gha,scope=test
5151
cache-to: type=gha,mode=max,scope=test
5252
- name: Test tgbot-cpp
53-
run: make docker-test-only
53+
run: |
54+
docker run --name tgbot-cpp-coverage reo7sp/tgbot-cpp-test sh -eu -c '
55+
make test-only
56+
make coverage
57+
'
58+
docker cp tgbot-cpp-coverage:/usr/src/tgbot-cpp/build/Debug/coverage ./coverage
59+
docker rm tgbot-cpp-coverage
5460
- name: Test codegen
5561
run: make docker-test-api-codegen
62+
- name: Upload HTML coverage report
63+
uses: actions/upload-artifact@v4
64+
with:
65+
name: coverage-html
66+
path: coverage
67+
- name: Upload coverage to Coveralls
68+
uses: coverallsapp/github-action@v2
69+
with:
70+
github-token: ${{ secrets.GITHUB_TOKEN }}
71+
file: coverage/coveralls.json
72+
format: coveralls
5673

5774
test-windows:
5875
runs-on: windows-2022

‎CMakeLists.txt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
22
project(TgBot LANGUAGES CXX)
33

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

@@ -39,6 +41,20 @@ target_compile_features(TgBot PUBLIC cxx_std_20)
3941
target_compile_options(TgBot PRIVATE
4042
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall>
4143
)
44+
if(ENABLE_SANITIZERS)
45+
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
46+
message(FATAL_ERROR "ENABLE_SANITIZERS requires GCC or Clang")
47+
endif()
48+
target_compile_options(TgBot PUBLIC -fsanitize=address,undefined -fno-omit-frame-pointer)
49+
target_link_options(TgBot PUBLIC -fsanitize=address,undefined)
50+
endif()
51+
if(ENABLE_COVERAGE)
52+
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
53+
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC")
54+
endif()
55+
target_compile_options(TgBot PRIVATE -O0 -g --coverage)
56+
target_link_options(TgBot PUBLIC --coverage)
57+
endif()
4258
target_include_directories(TgBot PUBLIC
4359
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
4460
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>

‎Dockerfile_test‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,24 @@ RUN apt-get -qq update && \
1515
python3-venv >/dev/null && \
1616
rm -rf /var/lib/apt/lists/*
1717

18-
ENV CONAN_BUILD_ARGS="--build=missing:gtest/*"
19-
2018
RUN python3 -m venv /opt/conan && \
2119
/opt/conan/bin/pip install --quiet --disable-pip-version-check --no-cache-dir \
2220
"clang-format==22.1.8" \
2321
"conan==2.31.1" \
22+
"gcovr==8.6" \
2423
"poetry==2.4.1"
2524
ENV PATH="/opt/conan/bin:${PATH}"
2625

2726
WORKDIR /usr/src/tgbot-cpp
2827

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

30+
ENV BUILD_TYPE=Debug
31+
ENV ENABLE_SANITIZERS=ON
32+
ENV ENABLE_COVERAGE=ON
33+
ENV ASAN_OPTIONS=detect_leaks=1:halt_on_error=1
34+
ENV UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
35+
3136
RUN make dependencies-with-test
3237

3338
COPY include include

‎Makefile‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ CONAN_RECIPE_ARGS ?=
88
CMAKE_LOG_LEVEL ?= STATUS
99
CMAKE_INSTALL_MESSAGE ?= ALWAYS
1010
CMAKE_BUILD_ARGS ?=
11+
ENABLE_SANITIZERS ?= OFF
12+
ENABLE_COVERAGE ?= OFF
1113
POETRY_INSTALL_ARGS ?= --no-interaction --quiet
1214
INSTALL_PREFIX ?=
1315
API_METHODS_CPP = src/ApiMethods.cpp
@@ -23,6 +25,7 @@ EXAMPLES := $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard examples/*/CMakeLis
2325
EXAMPLE_IMAGE ?= tgbot-cpp-example-$(EXAMPLE)
2426
PORT ?= 8080
2527
DOCS_WORKTREE := $(abspath build/gh-pages)
28+
COVERAGE_REPORT_DIR ?= $(BUILD_DIR)/coverage
2629

2730
ifeq ($(OS),Windows_NT)
2831
export CMAKE_GENERATOR ?= Ninja
@@ -46,6 +49,7 @@ endif
4649
test \
4750
test-only \
4851
test-api-codegen \
52+
coverage \
4953
install \
5054
install-with-system \
5155
install-only \
@@ -94,13 +98,17 @@ configure: dependencies
9498
-DCMAKE_TOOLCHAIN_FILE=$(abspath $(BUILD_DIR)/generators/conan_toolchain.cmake) \
9599
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
96100
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
101+
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
102+
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
97103
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
98104
-DENABLE_TESTS=OFF
99105

100106
configure-with-system:
101107
cmake --log-level=$(CMAKE_LOG_LEVEL) -S . -B $(BUILD_DIR) \
102108
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
103109
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
110+
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
111+
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
104112
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
105113
-DENABLE_TESTS=OFF
106114

@@ -109,6 +117,8 @@ configure-with-test: dependencies-with-test
109117
-DCMAKE_TOOLCHAIN_FILE=$(abspath $(BUILD_DIR)/generators/conan_toolchain.cmake) \
110118
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
111119
-DCMAKE_INSTALL_MESSAGE=$(CMAKE_INSTALL_MESSAGE) \
120+
-DENABLE_SANITIZERS=$(ENABLE_SANITIZERS) \
121+
-DENABLE_COVERAGE=$(ENABLE_COVERAGE) \
112122
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
113123
-DENABLE_TESTS=ON
114124

@@ -147,6 +157,17 @@ test-only:
147157
test-api-codegen:
148158
poetry run pytest api_codegen/tests
149159

160+
coverage:
161+
cmake -E make_directory $(COVERAGE_REPORT_DIR)
162+
gcovr $(BUILD_DIR) \
163+
--root . \
164+
--filter 'include/tgbot/' \
165+
--filter 'src/' \
166+
--coveralls $(COVERAGE_REPORT_DIR)/coveralls.json \
167+
--lcov $(COVERAGE_REPORT_DIR)/lcov.info \
168+
--html-details $(COVERAGE_REPORT_DIR)/coverage.html \
169+
--print-summary
170+
150171
install: build
151172
$(MAKE) install-only
152173

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# tgbot-cpp
22

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

56
C++ library for Telegram bot API.
67

‎api/codegen.yaml‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ types:
5252
api:
5353
addStickerToSet:
5454
args_order: [user_id, name, sticker]
55+
supports_attach_references: true
5556
answerCallbackQuery:
5657
args_order: [callback_query_id, text, show_alert, url, cache_time]
5758
answerInlineQuery:
@@ -80,6 +81,7 @@ api:
8081
provider_token: {declaration_required: true}
8182
createNewStickerSet:
8283
args_order: [user_id, name, title, stickers, sticker_type, needs_repainting]
84+
supports_attach_references: true
8385
args:
8486
sticker_type: {type: "Sticker::Type", default: "Sticker::Type::Regular"}
8587
deleteMyCommands:
@@ -97,12 +99,16 @@ api:
9799
editMessageMedia:
98100
return_type: std::shared_ptr<Message>
99101
args_order: [media, chat_id, message_id, inline_message_id, reply_markup]
102+
supports_attach_references: true
100103
editMessageReplyMarkup:
101104
return_type: std::shared_ptr<Message>
102105
args_order: [chat_id, message_id, inline_message_id, reply_markup]
103106
editMessageText:
104107
return_type: std::shared_ptr<Message>
105108
args_order: [text, chat_id, message_id, inline_message_id, parse_mode, link_preview_options, reply_markup, entities]
109+
supports_attach_references: true
110+
editStory:
111+
supports_attach_references: true
106112
forwardMessage:
107113
args_order: [chat_id, from_chat_id, message_id, disable_notification, protect_content, message_thread_id]
108114
forwardMessages:
@@ -123,6 +129,7 @@ api:
123129
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]
124130
replaceStickerInSet:
125131
args_order: [user_id, name, old_sticker, sticker]
132+
supports_attach_references: true
126133
restrictChatMember:
127134
args_order: [chat_id, user_id, permissions, until_date, use_independent_chat_permissions]
128135
sendAnimation:
@@ -146,7 +153,12 @@ api:
146153
sendLocation:
147154
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]
148155
sendMediaGroup:
149-
args_order: [chat_id, media, disable_notification, reply_parameters, message_thread_id, protect_content, business_connection_id]
156+
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]
157+
supports_attach_references: true
158+
sendPaidMedia:
159+
supports_attach_references: true
160+
sendRichMessage:
161+
supports_attach_references: true
150162
sendMessage:
151163
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]
152164
sendPhoto:
@@ -165,11 +177,17 @@ api:
165177
args_order: [chat_id, video_note, reply_parameters, disable_notification, duration, length, thumbnail, reply_markup, message_thread_id, protect_content, business_connection_id]
166178
sendVoice:
167179
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]
180+
postStory:
181+
supports_attach_references: true
182+
setBusinessAccountProfilePhoto:
183+
supports_attach_references: true
168184
setChatAdministratorCustomTitle:
169185
args_order: [chat_id, user_id, custom_title]
170186
setGameScore:
171187
return_type: std::shared_ptr<Message>
172188
args_order: [user_id, score, force, disable_edit_message, chat_id, message_id, inline_message_id]
189+
setMyProfilePhoto:
190+
supports_attach_references: true
173191
setMessageReaction:
174192
args_order: [chat_id, message_id, reaction, is_big]
175193
setMyCommands:

‎api_codegen/generate.py‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
CONFIG = yaml.safe_load(DEFAULT_CONFIG.read_text(encoding="utf-8"))
2525
TYPE_CONFIG = CONFIG.get("types", {})
2626
API_CONFIG = CONFIG.get("api", {})
27+
ATTACH_REFERENCES_DESCRIPTION = (
28+
"Files uploaded as named multipart parts. Reference each file from a composite "
29+
"Telegram API argument as attach://<name> and use the same name in InputFileAttachment."
30+
)
2731

2832

2933
def run(schema_path: Path, root: Path) -> None:
@@ -99,6 +103,7 @@ class _MethodModel:
99103
return_description: str
100104
description: tuple[str, ...]
101105
args: tuple[_ArgModel, ...]
106+
compatibility_args: tuple[_ArgModel, ...]
102107
args_name: str | None
103108

104109

@@ -364,11 +369,14 @@ def _build_method(self, operation: Schema) -> _MethodModel:
364369
properties = dict(request.get("properties", {}))
365370
for arg_name, arg in multipart.get("properties", {}).items():
366371
properties.setdefault(arg_name, arg)
372+
method_config = self._api_config.get(name, {})
373+
if method_config.get("supports_attach_references"):
374+
properties["attachments"] = {"description": ATTACH_REFERENCES_DESCRIPTION}
375+
properties.update(method_config.get("extra_args", {}))
367376
required = set(request.get("required", [])) | set(multipart.get("required", []))
368377
binary = self._binary_args(operation)
369378
arg_names = self._ordered_arg_names(name, properties, required)
370379
response_type = _CppTypeResolver.api_type(self._response_schema(operation))
371-
method_config = self._api_config.get(name, {})
372380
return_type = method_config.get("return_type", response_type)
373381
args = tuple(
374382
self._build_arg(
@@ -380,13 +388,15 @@ def _build_method(self, operation: Schema) -> _MethodModel:
380388
)
381389
for arg_name in arg_names
382390
)
391+
compatibility_without = set(method_config.get("compatibility_overload_without", ()))
383392
return _MethodModel(
384393
name=name,
385394
return_type=return_type,
386395
response_type=response_type,
387396
return_description=self._return_description(return_type, response_type),
388397
description=self._comment_lines(operation.get("description", ""), 88),
389398
args=args,
399+
compatibility_args=tuple(arg for arg in args if arg.wire_name not in compatibility_without),
390400
args_name=f"{name[0].upper()}{name[1:]}Args" if args else None,
391401
)
392402

@@ -411,7 +421,13 @@ def _ordered_arg_names(
411421
),
412422
)
413423
optional_names = [name for name in properties if name not in declaration_required]
414-
optional_names.sort(key=lambda name: (args_order.get(name, len(args_order)), name))
424+
optional_names.sort(
425+
key=lambda name: (
426+
name == "attachments" and method_config.get("supports_attach_references"),
427+
args_order.get(name, len(args_order)),
428+
name,
429+
)
430+
)
415431

416432
return required_names + optional_names
417433

@@ -424,7 +440,9 @@ def _build_arg(
424440
binary: bool,
425441
) -> _ArgModel:
426442
arg_config = self._api_config.get(method_name, {}).get("args", {}).get(name, {})
427-
override = arg_config.get("type")
443+
method_config = self._api_config.get(method_name, {})
444+
attach_references_arg = name == "attachments" and method_config.get("supports_attach_references")
445+
override = "std::vector<InputFileAttachment>" if attach_references_arg else arg_config.get("type")
428446
if override:
429447
cpp_type = override
430448
elif name in {"chat_id", "from_chat_id"}:
@@ -499,7 +517,9 @@ def _return_description(return_type: str, response_type: str) -> str:
499517

500518
@staticmethod
501519
def _arg_declaration_type(cpp_type: str) -> str:
502-
if cpp_type == "std::string" or cpp_type.startswith("std::vector<"):
520+
if cpp_type == "std::string":
521+
return "std::string_view"
522+
if cpp_type.startswith("std::vector<"):
503523
return f"const {cpp_type}&"
504524
if cpp_type == "nlohmann::json":
505525
return "const nlohmann::json&"

‎api_codegen/templates/api_methods.cpp.j2‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
ApiRequest::{{ factory }}("{{ arg.wire_name }}", {{ arg.cpp_name }}{{ wire_default }})
1515
{%- endmacro %}
1616

17-
{% macro method_definition(method, argument_object=false) -%}
17+
{% macro method_definition(method, argument_object=false, args=none) -%}
18+
{% set definition_args = method.args if args is none else args -%}
1819
{{ method.return_type }} Api::{{ method.name }}(
1920
{% if argument_object %}
2021
const {{ method.args_name }}& args
2122
{% else %}
22-
{% for arg in method.args %}
23+
{% for arg in definition_args %}
2324
{{ arg.declaration_type }} {{ arg.cpp_name }}{{ "," if not loop.last else "" }}
2425
{% endfor %}
2526
{% endif %}
@@ -47,6 +48,16 @@ namespace TgBot {
4748
);
4849
}
4950

51+
{% if method.compatibility_args != method.args %}
52+
{{ method_definition(method, args=method.compatibility_args) }} {
53+
return {{ method.name }}(
54+
{% for arg in method.args %}
55+
{{ arg.cpp_name if arg in method.compatibility_args else "{ }" }}{{ "," if not loop.last else "" }}
56+
{% endfor %}
57+
);
58+
}
59+
60+
{% endif %}
5061
{% if method.args_name %}
5162
{{ method_definition(method, argument_object=true) }} {
5263
return {{ method.name }}(

0 commit comments

Comments
 (0)