diff --git a/CMakeLists.txt b/CMakeLists.txt index ca33532..ead2b5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ SET(PREFIX ${CMAKE_INSTALL_PREFIX}) SET(EXEC_PREFIX "${PREFIX}/bin") SET(INCLUDEDIR "${PREFIX}/include/${PROJECT_NAME}") SET(LIBDIR "${PREFIX}/lib") -SET(VERSION 4.7.0) +SET(VERSION 4.7.1) SET(CMAKE_MACOSX_RPATH 1) @@ -16,7 +16,7 @@ SET(SRCS src/telebot.c ) -ADD_DEFINITIONS("-DDEBUG=1") +ADD_DEFINITIONS("-DDEBUG=0") INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/include) SET(DEPENDENTS "libcurl json-c") INCLUDE(FindPkgConfig) @@ -25,7 +25,7 @@ pkg_check_modules(PKGS REQUIRED ${DEPENDENTS}) FOREACH(flag ${PKGS_CFLAGS}) SET(EXTRA_LIB_CFLAGS "${EXTRA_LIB_CFLAGS} ${flag}") ENDFOREACH(flag) -SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${EXTRA_LIB_CFLAGS} -Werror -Wall -Wno-unused-function" ) +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${EXTRA_LIB_CFLAGS} -Werror -Wall -Wno-unused-function -O2" ) # libtelebot ADD_LIBRARY(${PROJECT_NAME} SHARED ${SRCS}) diff --git a/include/telebot-core.h b/include/telebot-core.h index cec6ef2..0938e87 100644 --- a/include/telebot-core.h +++ b/include/telebot-core.h @@ -440,6 +440,27 @@ telebot_error_e telebot_core_send_video_note(telebot_core_handler_t *core_h, const char *thumb, bool disable_notification, int reply_to_message_id, const char *reply_markup, telebot_core_response_t *response); +/** + * @brief Send a group of photos as an album. + * + * @param[in] core_h The telebot core handler created with #telebot_core_create(). + * @param[in] chat_id Unique identifier for the target chat or username of the target channel. + * @param[in] media_paths Array of file paths to photos to send. + * @param[in] count Number of photos in the array (2–10). + * @param[in] disable_notification Sends the message silently. Users will receive a notification with no sound. + * @param[in] reply_to_message_id If the message is a reply, ID of the original message. + * @param[out] response Response data that contains the sent messages on success. It MUST be freed with #telebot_core_put_response(). + * @return on Success, TELEBOT_ERROR_NONE is returned, otherwise a negative error value. + */ +telebot_error_e telebot_core_send_media_group( + telebot_core_handler_t *core_h, + long long int chat_id, + char *media_paths[], + int count, + bool disable_notification, + int reply_to_message_id, + telebot_core_response_t *response); + /** * @brief Send point on the map. * @param[in] core_h The telebot core handler created with #telebot_core_create(). diff --git a/include/telebot-methods.h b/include/telebot-methods.h index 7a7e90e..1a5730d 100644 --- a/include/telebot-methods.h +++ b/include/telebot-methods.h @@ -446,6 +446,25 @@ telebot_error_e telebot_send_video_note(telebot_handler_t handle, long long int char *video_note, bool is_file, int duration, int length, const char *thumb, bool disable_notification, int reply_to_message_id, const char *reply_markup); +/** + * @brief Send a group of photos as an album. + * + * @param[in] handle The telebot handler created with #telebot_create(). + * @param[in] chat_id Unique identifier for the target chat or username of the target channel. + * @param[in] media_paths Array of file paths to photos to send. + * @param[in] count Number of photos in the array (2–10). + * @param[in] disable_notification Sends the message silently. + * @param[in] reply_to_message_id If the message is a reply, ID of the original message. + * @return on Success, TELEBOT_ERROR_NONE is returned, otherwise a negative error value. + */ +telebot_error_e telebot_send_media_group( + telebot_handler_t handle, + long long int chat_id, + char *media_paths[], + int count, + bool disable_notification, + int reply_to_message_id); + /** * @brief Send point on the map. * @param[in] handle The telebot handler created with #telebot_create(). diff --git a/include/telebot-types.h b/include/telebot-types.h index 5ba16a9..1f06e81 100644 --- a/include/telebot-types.h +++ b/include/telebot-types.h @@ -1242,7 +1242,7 @@ typedef struct telebot_update { * A user changed their answer in a non-anonymous poll. Bots receive * new votes only in polls that were sent by the bot itself. */ - telebot_poll_answer_t poll_anser; + telebot_poll_answer_t poll_answer; }; } telebot_update_t; diff --git a/src/estgb.c b/src/estgb.c new file mode 100644 index 0000000..7194ea3 --- /dev/null +++ b/src/estgb.c @@ -0,0 +1,832 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0])) +#define TOKEN_SIZE 128 +#define FILENAME_TOKEN ".token" +#define FILENAME_USERID ".userid" +#define PID_FILE "/var/tmp/estgb_lock.pid" + +typedef void (*sendfunc_t)(char *filename); +typedef void (*sendfuncgroup_t)(char **filenames, int count); + +struct { + char *token; + long long user_id; + char *imgfile; + char *videofile; + char *docfile; + char *audiofile; + char *comment; + char *path; + char *text; + char *mask; + char *proxy_addr; + char *proxy_auth; + int isAnimation; + int isDaemonize; + int isRemove; + int isWeakConfig; + int isWildcard; + int isSingleton; + int isScan; + int needUnescape; + int useFileConfig; + int isPicsMediagroup; + int timeRescan; + int fd; + telebot_handler_t handle; + telebot_error_e ret; +} estgbconf; + +char* concatFilename(char *path, char *filename) { + int isSlash = 2; + char *result = NULL; + size_t path_len = strlen(path); + + if (path_len > 0 && path[path_len - 1] == '\\') + isSlash = 1; + + if (isSlash == 2) + asprintf(&result, "%s/%s", path, filename); + else + asprintf(&result, "%s%s", path, filename); + + return result; +} + +int checkSingleton() { + estgbconf.fd = open(PID_FILE, O_CREAT | O_RDWR, 0666); + if (estgbconf.fd == -1) + return 2; + + int rc = flock(estgbconf.fd, LOCK_EX | LOCK_NB); + if (rc) { + if (EWOULDBLOCK == errno) + return 1; + } + return 0; +} + +int checkMask(const struct dirent *dp) { + return (fnmatch(estgbconf.mask, dp->d_name, 0) == 0); +} + +void processWildcard_s(char *path, sendfunc_t sendfunc) { + struct dirent **namelist; + int n; + char *fullname; + + n = scandir(path, &namelist, checkMask, alphasort); + if (n < 0) + printf("Scandir error\n"); + else { + + while (n--) { + fullname = concatFilename(path, namelist[n]->d_name); + sendfunc(fullname); + free(fullname); + free(namelist[n]); + } + free(namelist); + } +} + +void processWildcard_group(char *path, sendfuncgroup_t sendfunc) { + struct dirent **namelist; + int n, j; + char **sendlist; + + n = scandir(path, &namelist, checkMask, alphasort); + if (n < 0) + printf("Scandir error\n"); + else { + + sendlist = calloc(n, sizeof(char*)); + + j = n; + while (n--) { + sendlist[n] = concatFilename(path, namelist[n]->d_name); + } + sendfunc(sendlist, j); + while (j--) { + free(namelist[j]); + free(sendlist[j]); + } + free(namelist); + free(sendlist); + } +} + +// Code of this function grabbed from source code of 'echo' utility +// https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/echo.c +inline static int hex2bin(unsigned char c) { + switch (c) { + default: + return c - '0'; + case 'a': + case 'A': + return 10; + case 'b': + case 'B': + return 11; + case 'c': + case 'C': + return 12; + case 'd': + case 'D': + return 13; + case 'e': + case 'E': + return 14; + case 'f': + case 'F': + return 15; + } +} + +// Zero-copy unescape function +// partially based on canonical 'echo' utility source code +// https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/echo.c +char* zc_unescape(char *input) { + unsigned char c; + char *p, *s; + + if (input == NULL) + return NULL; + + s = input; + p = input; + + while ((c = *s++)) { + if (c == '\\' && *s) { + switch (c = *s++) { + case 'e': + c = '\x1B'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = '\v'; + break; + case 'x': { + unsigned char ch = *s; + if (!isxdigit(ch)) + goto not_an_escape; + s++; + c = hex2bin(ch); + ch = *s; + if (isxdigit(ch)) { + s++; + c = c * 16 + hex2bin(ch); + } + } + break; + case '0': + c = 0; + if (!('0' <= *s && *s <= '7')) + break; + c = *s++; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + c -= '0'; + if ('0' <= *s && *s <= '7') + c = c * 8 + (*s++ - '0'); + if ('0' <= *s && *s <= '7') + c = c * 8 + (*s++ - '0'); + break; + case '\\': + break; + + not_an_escape: default: + *p = c; + p++; + break; + } + } + *p = c; + p++; + } + *p = '\0'; + + return input; +} + +void printHelp(void) { + printf( + "-----------------------------------------------------------------------------\n" + "| estgb :: enhanced sender telegram bot v1.4.0|\n" + "-----------------------------------------------------------------------------\n" + "\n" + "This telegram bot sends text, pictures, video, audio and documents (files)\n" + "according to command line paramaters\n" + "(c) 2018-2026 Flangeneer, Saint-Petersburg, Russia\n" + "\n" + "Usage: estgb [options]\n" + "\n" + "Commands:\n" + "--sendtext Send text\n" + "--sendpic Send picture\n" + "--senddoc Send document\n" + "--sendvideo Send video\n" + "--sendaudio Send audio\n" + "\n" + "Options for Telegram bot configuration (required):\n" + "--token Bot token\n" + "--userid User ID\n" + " or use:\n" + "--fileconfigs Read bot token and userid from files %s and %s accordingly\n" + "--path Working path for --fileconfigs option\n" + "\n" + "to use '--fileconfigs' there are should be two files:\n" + "1) %s - text file where first string is telegram bot token\n" + "2) %s - text file where first string is user ID\n" + "\n" + "Options to proceed the text and media:\n" + "--animation Send video as animation (GIF or H.264/MPEG-4 w/o sound mp4)\n" + "--comment Comment for picture/audio/video\n" + "--escape-seq Process escape sequences (emoji!) for 'comment' and 'text' (C-style)\n" + "--mediagroup Send multiple pictures as media group (when 2-10 items are exist)\n" + "\n" + "Options for network:\n" + "--proxy Use libcurl proxy. Examples: socks5://addr.org:8564 or http://addr.org:8564\n" + "--proxyauth Specify username and password for proxy\n" + "\n" + "Options for file processing:\n" + "--wildcard Process as wildcard instead of single file\n" + "--remove Remove(!) file after use for --sendpic, --sendvideo, --sendaudio, --senddoc\n" + "--force-remove Force remove(!) file after use even(!) error occured while send operation\n" + "\n" + "Options other:\n" + "--repeat-send Repeat send procedure N rounds. Specify 0 (zero) to infinite\n" + "--time-sleep Time to sleep betwen cycles in seconds. Default 1 sec\n" + "--singleton If another instance of bot is working, do nothing and exit\n" + "--daemon Daemonize process (process work in background)\n" + "--weakconfig [FOR DEBUG] Simplified command line parameters pre-check\n" + "\n", FILENAME_TOKEN, FILENAME_USERID, FILENAME_TOKEN, + FILENAME_USERID); + +} + +void initConfig(void) { + memset(&estgbconf, 0, sizeof(estgbconf)); + estgbconf.timeRescan = 1; + estgbconf.isScan = -1; +} + +void freeConfig(void) { + telebot_destroy(estgbconf.handle); + free(estgbconf.token); + free(estgbconf.text); + free(estgbconf.imgfile); + free(estgbconf.videofile); + free(estgbconf.audiofile); + free(estgbconf.docfile); + free(estgbconf.path); + free(estgbconf.comment); + free(estgbconf.proxy_addr); + free(estgbconf.proxy_auth); + close(estgbconf.fd); +} + +void printConfig(void) { + printf("Current config:\n" + "Bot token...............%s\n" + "User ID.................%lld\n" + "Image filename..........%s\n" + "Video filename..........%s\n" + "Audio filename..........%s\n" + "Doc filename............%s\n" + "Video as animation......%d\n" + "Send >2 pics as group...%d\n" + "Comment.................%s\n" + "Parse escape sequences..%d\n" + "Text to send............%s\n" + "Remove file after use...%d\n" + "Use file config.........%d\n" + "Working path............%s\n" + "Use wildcards...........%d\n" + "Use weak config.........%d\n" + "Run as singleton........%d\n" + "Work as daemon..........%d\n" + "Proxy...................%s\n" + "Proxy authentication....%s\n" + "Send rounds.............%d\n" + "Time sleep btw rounds...%d\n" + "\n", estgbconf.token, estgbconf.user_id, estgbconf.imgfile, + estgbconf.videofile, estgbconf.audiofile, estgbconf.docfile, + estgbconf.isAnimation, estgbconf.isPicsMediagroup, + estgbconf.comment, estgbconf.needUnescape, estgbconf.text, + estgbconf.isRemove, estgbconf.useFileConfig, estgbconf.path, + estgbconf.isWildcard, estgbconf.isWeakConfig, estgbconf.isSingleton, + estgbconf.isDaemonize, estgbconf.proxy_addr, estgbconf.proxy_auth, + estgbconf.isScan, estgbconf.timeRescan); +} + +void sendText(void) { + if (estgbconf.text != NULL) { + estgbconf.ret = telebot_send_message(estgbconf.handle, + estgbconf.user_id, estgbconf.text, "", false, false, 0, ""); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send text message: %d \n", estgbconf.ret); + } + } +} + +void sendPicture(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_photo( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + estgbconf.comment, + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send picture: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void removeFileGroup(char **filenames, int count) { + int j = count; + while (j--) + remove(filenames[j]); +} + +void sendGroup(char **filenames, int count) { + estgbconf.ret = telebot_send_media_group( + estgbconf.handle, + estgbconf.user_id, + filenames, + count, + false, // disable_notification + 0 // reply_to_message_id + // ← NO reply_markup (not supported by Telegram API for media group) + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send media group: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + removeFileGroup(filenames, count); + if (estgbconf.isRemove == 2) + removeFileGroup(filenames, count); +} + +void sendPicturesGroup(char **filenames, int count) { + + int i, j = 0; + + if (count == 1) { + sendPicture(filenames[0]); + return; + } + + char **sendlist; + sendlist = calloc(10, sizeof(char*)); + + for (i = 0; i < count; i++) { + + if (!access(filenames[i], R_OK)) { + sendlist[j] = strdup(filenames[i]); + j++; + } + + if (j == 10) { + sendGroup(sendlist, j); + while (j--) + free(sendlist[j]); + + j = 0; + } + } + + if (j > 0) { + sendGroup(sendlist, j); + while (j--) + free(sendlist[j]); + } + free(sendlist); +} + +void sendVideo(char *filename) { + if (!access(filename, R_OK)) { + if (estgbconf.isAnimation) + estgbconf.ret = telebot_send_animation( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + 0, 0, 0, // duration, width, height + NULL, // thumb + estgbconf.comment, + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + else + estgbconf.ret = telebot_send_video( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + 0, 0, 0, // duration, width, height + NULL, // thumb + estgbconf.comment, + NULL, // parse_mode + false, // supports_streaming + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send video: %d \n", estgbconf.ret); + + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendAudio(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_audio( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + estgbconf.comment, // caption + NULL, // parse_mode + 0, // duration + NULL, // performer + NULL, // title + NULL, // thumb + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send audio: %d \n", estgbconf.ret); + + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendDocument(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_document( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + NULL, // thumb + estgbconf.comment, // caption + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send document: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendSingle(void) { + sendText(); + if (estgbconf.imgfile) + sendPicture(estgbconf.imgfile); + if (estgbconf.videofile) + sendVideo(estgbconf.videofile); + if (estgbconf.audiofile) + sendAudio(estgbconf.audiofile); + if (estgbconf.docfile) + sendDocument(estgbconf.docfile); +} + +void sendMultiple(void) { + + char *imgmask = NULL, *imgdir = NULL; + char *vidmask = NULL, *viddir = NULL; + char *audiomask = NULL, *auddir = NULL; + char *docmask = NULL, *docdir = NULL; + + sendText(); + + if (estgbconf.imgfile) { + imgmask = strdup(basename(estgbconf.imgfile)); + imgdir = strdup(estgbconf.imgfile); + imgdir = dirname(imgdir); + estgbconf.mask = imgmask; + if (estgbconf.isPicsMediagroup) + processWildcard_group(imgdir, sendPicturesGroup); + else processWildcard_s(imgdir, sendPicture); + } + + if (estgbconf.videofile) { + vidmask = strdup(basename(estgbconf.videofile)); + viddir = strdup(estgbconf.videofile); + viddir = dirname(viddir); + estgbconf.mask = vidmask; + processWildcard_s(viddir, sendVideo); + } + + if (estgbconf.audiofile) { + audiomask = strdup(basename(estgbconf.audiofile)); + auddir = strdup(estgbconf.audiofile); + auddir = dirname(auddir); + estgbconf.mask = audiomask; + processWildcard_s(auddir, sendAudio); + } + + if (estgbconf.docfile) { + docmask = strdup(basename(estgbconf.docfile)); + docdir = strdup(estgbconf.docfile); + docdir = dirname(docdir); + estgbconf.mask = docmask; + processWildcard_s(docdir, sendDocument); + } + + free(imgmask); + free(vidmask); + free(audiomask); + free(docmask); + free(imgdir); + free(viddir); + free(auddir); + free(docdir); + estgbconf.mask = NULL; +} + +void scan(void) { + + int infinite = (estgbconf.isScan == 0); + if (estgbconf.isScan < 0) + return; + + if (!estgbconf.isWildcard) { + while (estgbconf.isScan-- || infinite) { + sendSingle(); + sleep(estgbconf.timeRescan); + } + return; + } + + while (estgbconf.isScan-- || infinite) { + sendMultiple(); + sleep(estgbconf.timeRescan); + } + return; +} + +int readFileConfig(void) { + + char *tokenfilename, *useridfilename; + + if (estgbconf.path == NULL) { + tokenfilename = strdup(FILENAME_TOKEN); + useridfilename = strdup(FILENAME_USERID); + } else { + tokenfilename = concatFilename(estgbconf.path, FILENAME_TOKEN); + useridfilename = concatFilename(estgbconf.path, FILENAME_USERID); + } + + FILE *fp = fopen(tokenfilename, "r"); + if (fp == NULL) { + printf("Failed to open %s file\n", FILENAME_TOKEN); + return -1; + } + + estgbconf.token = (char*) calloc(TOKEN_SIZE, sizeof(char)); + if (fscanf(fp, "%s", estgbconf.token) == 0) { + printf("Reading token failed\n"); + fclose(fp); + return -1; + } + fclose(fp); + + fp = fopen(useridfilename, "r"); + if (fp == NULL) { + printf("Failed to open %s file\n", FILENAME_USERID); + return -1; + } + + if (fscanf(fp, "%lld", &estgbconf.user_id) == 0) { + printf("Reading user ID failed\n"); + fclose(fp); + return -1; + } + + free(tokenfilename); + free(useridfilename); + fclose(fp); + return 0; +} + +int checkConfig(void) { + if (estgbconf.token == NULL) + return 0; + if (estgbconf.user_id == 0) + return 0; + if ((estgbconf.imgfile == NULL) && (estgbconf.text == NULL) + && (estgbconf.videofile == NULL) && (estgbconf.docfile == NULL) + && (estgbconf.audiofile == NULL)) + return 0; + + if (!estgbconf.isWeakConfig && (estgbconf.imgfile == NULL) + && (estgbconf.videofile == NULL) && (estgbconf.docfile == NULL) + && (estgbconf.audiofile == NULL) && (estgbconf.isRemove != 0)) + return 0; + return 1; +} + +int globalInit(void) { + if (estgbconf.useFileConfig) { + if (readFileConfig() != 0) { + printf( + "Warning! Failed to read config files. Checking for command line config...\n"); + } + } + + if (!checkConfig()) { + printHelp(); + printConfig(); + printf("Configuration error. Nothing to do.\n\n"); + return 0; + } + + if (telebot_create(&estgbconf.handle, estgbconf.token) + != TELEBOT_ERROR_NONE) { + printf("Error. Telebot create failed (bad token?)\n\n"); + return 0; + } + + if (estgbconf.proxy_addr != NULL) { + estgbconf.ret = telebot_set_proxy(estgbconf.handle, + estgbconf.proxy_addr, estgbconf.proxy_auth); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Warning! Failed to init proxy: %d \n", estgbconf.ret); + } + } + + return 1; +} + +int parseCmdLine(int argc, char *argv[]) { + int j, more; + + if (argc < 3) { + printHelp(); + return 0; + } + + for (j = 1; j < argc; j++) { + more = ((j + 1) < argc); + if (!strcmp(argv[j], "--remove")) { + estgbconf.isRemove = 1; + } else if (!strcmp(argv[j], "--force-remove")) { + estgbconf.isRemove = 2; + } else if (!strcmp(argv[j], "--escape-seq")) { + estgbconf.needUnescape = 1; + } else if (!strcmp(argv[j], "--singleton")) { + estgbconf.isSingleton = 1; + } else if (!strcmp(argv[j], "--daemon")) { + estgbconf.isDaemonize = 1; + } else if (!strcmp(argv[j], "--mediagroup")) { + estgbconf.isPicsMediagroup = 1; + } else if (!strcmp(argv[j], "--repeat-send") && more) { + estgbconf.isScan = atoi(argv[++j]); + } else if (!strcmp(argv[j], "--time-sleep") && more) { + estgbconf.timeRescan = atoi(argv[++j]); + } else if (!strcmp(argv[j], "--animation")) { + estgbconf.isAnimation = 1; + } else if (!strcmp(argv[j], "--wildcard")) { + estgbconf.isWildcard = 1; + } else if (!strcmp(argv[j], "--weakconfig")) { + estgbconf.isWeakConfig = 1; + } else if (!strcmp(argv[j], "--sendpic") && more) { + estgbconf.imgfile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendvideo") && more) { + estgbconf.videofile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendtext") && more) { + estgbconf.text = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--senddoc") && more) { + estgbconf.docfile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendaudio") && more) { + estgbconf.audiofile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--fileconfigs")) { + estgbconf.useFileConfig = 1; + } else if (!strcmp(argv[j], "--token") && more) { + estgbconf.token = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--userid") && more) { + estgbconf.user_id = atoll(argv[++j]); + } else if (!strcmp(argv[j], "--path") && more) { + estgbconf.path = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--comment") && more) { + estgbconf.comment = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--proxy") && more) { + estgbconf.proxy_addr = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--proxyauth") && more) { + estgbconf.proxy_auth = strdup(argv[++j]); + } + + else if (!strcmp(argv[j], "--help")) { + printHelp(); + return 0; + } else { + printHelp(); + printConfig(); + printf("Unknown or not enough arguments for option '%s'.\n\n", + argv[j]); + return 0; + } + } + return 1; +} + +int main(int argc, char *argv[]) { + + initConfig(); + if (!parseCmdLine(argc, argv)) + goto exit; + + if (estgbconf.isSingleton) { + switch (checkSingleton()) { + case 0: + break; // No another instance found, continue to work + case 1: + goto exit; + // Found another instance of bot, exiting + case 2: + printf("Error open PID file for singleton check\n"); + goto exit; + // Something goes wrong, exiting + } + + } + + if (!globalInit()) + goto exit; + + if (estgbconf.isDaemonize) + daemon(0, 0); + + if (estgbconf.needUnescape) { + estgbconf.text = zc_unescape(estgbconf.text); + estgbconf.comment = zc_unescape(estgbconf.comment); + } + + if (estgbconf.isScan >= 0) { + scan(); + goto exit; + } + + if (estgbconf.isWildcard) + sendMultiple(); + else + sendSingle(); + + exit: freeConfig(); + return 0; +} diff --git a/src/telebot-core.c b/src/telebot-core.c index da1d581..e6ddd64 100644 --- a/src/telebot-core.c +++ b/src/telebot-core.c @@ -1,3 +1,5 @@ +#define _GNU_SOURCE + /* * telebot * @@ -20,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -238,7 +241,7 @@ telebot_error_e telebot_core_set_proxy(telebot_core_handler_t *core_h, core_h->proxy_addr = strdup(addr); if (core_h->proxy_addr == NULL) { - ERR("Failed to allocate memor for proxy address"); + ERR("Failed to allocate memorу for proxy address"); return TELEBOT_ERROR_OUT_OF_MEMORY; } @@ -247,7 +250,7 @@ telebot_error_e telebot_core_set_proxy(telebot_core_handler_t *core_h, core_h->proxy_auth = strdup(auth); if (core_h->proxy_auth == NULL) { - ERR("Failed to allocate memor for proxy authorization"); + ERR("Failed to allocate memorу for proxy authorization"); TELEBOT_SAFE_FREE(core_h->proxy_addr); return TELEBOT_ERROR_OUT_OF_MEMORY; } @@ -727,7 +730,7 @@ telebot_error_e telebot_core_send_video(telebot_core_handler_t *core_h, { if ((core_h == NULL) || (core_h->token == NULL) || (video == NULL)) { - ERR("Handler, token or document is NULL"); + ERR("Handler, token or video is NULL"); return TELEBOT_ERROR_INVALID_PARAMETER; } @@ -827,7 +830,7 @@ telebot_error_e telebot_core_send_animation(telebot_core_handler_t *core_h, { if ((core_h == NULL) || (core_h->token == NULL) || (animation == NULL)) { - ERR("Handler, token or document is NULL"); + ERR("Handler, token or animation is NULL"); return TELEBOT_ERROR_INVALID_PARAMETER; } @@ -1056,6 +1059,253 @@ telebot_error_e telebot_core_send_video_note(telebot_core_handler_t *core_h, return telebot_core_curl_perform(core_h, TELEBOT_METHOD_SEND_VIDEO_NOTE, mimes, index, response); } +telebot_error_e telebot_core_send_media_group( + telebot_core_handler_t *core_h, + long long int chat_id, + char *media_paths[], + int count, + bool disable_notification, + int reply_to_message_id, + telebot_core_response_t *response) +{ + if ((core_h == NULL) || (core_h->token == NULL) || + (media_paths == NULL) || (count < 2) || (count > 10)) + { + ERR("Invalid parameters: core_h, token, media_paths, or count (%d)", count); + return TELEBOT_ERROR_INVALID_PARAMETER; + } + + // Validate all media paths are non-NULL + for (int i = 0; i < count; ++i) + { + if (media_paths[i] == NULL) + { + ERR("Media path at index %d is NULL", i); + return TELEBOT_ERROR_INVALID_PARAMETER; + } + } + + // Build media array using json-c + struct json_object *media_array = json_object_new_array(); + if (media_array == NULL) + { + ERR("Failed to create JSON media array"); + return TELEBOT_ERROR_OUT_OF_MEMORY; + } + + // Allocate memory for filenames + char **filenames = calloc(count, sizeof(char*)); + if (filenames == NULL) + { + json_object_put(media_array); + ERR("Failed to allocate memory for filenames"); + return TELEBOT_ERROR_OUT_OF_MEMORY; + } + + // Helper function to determine media type based on file extension + const char* get_media_type(const char* filename) { + const char* ext = strrchr(filename, '.'); + if (ext == NULL) return "document"; // No extension, treat as document + + ext++; // Skip the dot + + // Convert to lowercase for comparison + char ext_lower[10]; + int i = 0; + while (ext[i] != '\0' && i < 9) { + ext_lower[i] = (ext[i] >= 'A' && ext[i] <= 'Z') ? ext[i] - 'A' + 'a' : ext[i]; + i++; + } + ext_lower[i] = '\0'; + + // Check for photo extensions + if (strcmp(ext_lower, "jpg") == 0 || strcmp(ext_lower, "jpeg") == 0 || + strcmp(ext_lower, "png") == 0 || strcmp(ext_lower, "bmp") == 0 || + strcmp(ext_lower, "tiff") == 0 || strcmp(ext_lower, "webp") == 0) { + return "photo"; + } + // Check for video extensions + else if (strcmp(ext_lower, "mp4") == 0 || strcmp(ext_lower, "mpeg") == 0 || + strcmp(ext_lower, "avi") == 0 || strcmp(ext_lower, "mov") == 0 || + strcmp(ext_lower, "mkv") == 0 || strcmp(ext_lower, "wmv") == 0 || + strcmp(ext_lower, "flv") == 0 || strcmp(ext_lower, "webm") == 0 || + strcmp(ext_lower, "3gp") == 0 || strcmp(ext_lower, "m4v") == 0) { + return "video"; + } + // Check for audio extensions + else if (strcmp(ext_lower, "mp3") == 0 || strcmp(ext_lower, "m4a") == 0 || + strcmp(ext_lower, "flac") == 0 || strcmp(ext_lower, "ogg") == 0 || + strcmp(ext_lower, "oga") == 0 || strcmp(ext_lower, "wav") == 0 || + strcmp(ext_lower, "aac") == 0 || strcmp(ext_lower, "opus") == 0) { + return "audio"; + } + // Everything else is treated as document (including gif) + else { + return "document"; + } + } + + // Determine media types for validation + const char **media_types = calloc(count, sizeof(char*)); + if (media_types == NULL) + { + for (int i = 0; i < count; i++) { + free(filenames[i]); + } + free(filenames); + json_object_put(media_array); + ERR("Failed to allocate memory for media types"); + return TELEBOT_ERROR_OUT_OF_MEMORY; + } + + for (int i = 0; i < count; ++i) + { + // Extract filename from path using basename + const char *filename = basename(media_paths[i]); + + // Allocate memory for filename + filenames[i] = strdup(filename); + if (filenames[i] == NULL) + { + // Free previously allocated resources + for (int j = 0; j < i; j++) + { + free(filenames[j]); + } + free(filenames); + free(media_types); + json_object_put(media_array); + ERR("Failed to duplicate filename"); + return TELEBOT_ERROR_OUT_OF_MEMORY; + } + + // Determine media type + media_types[i] = get_media_type(filename); + } + + // Validate media group composition + // Count unique types in the group + int photo_count = 0, video_count = 0, audio_count = 0, document_count = 0; + for (int i = 0; i < count; i++) { + if (strcmp(media_types[i], "photo") == 0) photo_count++; + else if (strcmp(media_types[i], "video") == 0) video_count++; + else if (strcmp(media_types[i], "audio") == 0) audio_count++; + else if (strcmp(media_types[i], "document") == 0) document_count++; + } + + // Check valid combinations: + // 1. All of the same type + // 2. Mixed photo and video only + bool valid_combination = false; + + if (photo_count == count || video_count == count || audio_count == count || document_count == count) { + // All same type - valid + valid_combination = true; + } else if (photo_count > 0 && video_count > 0 && audio_count == 0 && document_count == 0) { + // Mixed photo and video only - valid + valid_combination = true; + } + + if (!valid_combination) { + // Free allocated resources + for (int i = 0; i < count; i++) { + free(filenames[i]); + } + free(filenames); + free(media_types); + json_object_put(media_array); + ERR("Invalid media group composition: only homogeneous groups or mixed photo/video groups are allowed"); + return TELEBOT_ERROR_INVALID_PARAMETER; + } + + // Create JSON objects for media array + for (int i = 0; i < count; ++i) + { + struct json_object *item = json_object_new_object(); + json_object_object_add(item, "type", json_object_new_string(media_types[i])); + + // Create attach:// reference using snprintf instead of asprintf + char attach_ref[256]; // Sufficient size for "attach://" + filename + snprintf(attach_ref, sizeof(attach_ref), "attach://%s", filenames[i]); + json_object_object_add(item, "media", json_object_new_string(attach_ref)); + json_object_array_add(media_array, item); + } + + // Free temporary media types array + free(media_types); + + const char *media_json_str = json_object_to_json_string(media_array); + if (media_json_str == NULL) + { + // Free allocated filenames + for (int i = 0; i < count; i++) + { + free(filenames[i]); + } + free(filenames); + json_object_put(media_array); + ERR("Failed to serialize media JSON"); + return TELEBOT_ERROR_OPERATION_FAILED; + } + + // Prepare MIME parts + telebot_core_mime_t mimes[20]; // max: chat_id + media + disable_notif + reply_id + 10 files + int index = 0; + + // chat_id + mimes[index].name = "chat_id"; + mimes[index].type = TELEBOT_MIME_TYPE_DATA; + snprintf(mimes[index].data, sizeof(mimes[index].data), "%lld", chat_id); + ++index; + + // media (JSON string) + mimes[index].name = "media"; + mimes[index].type = TELEBOT_MIME_TYPE_DATA; + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", media_json_str); + ++index; + + // disable_notification + mimes[index].name = "disable_notification"; + mimes[index].type = TELEBOT_MIME_TYPE_DATA; + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", + (disable_notification ? "true" : "false")); + ++index; + + // reply_to_message_id (optional) + if (reply_to_message_id > 0) + { + mimes[index].name = "reply_to_message_id"; + mimes[index].type = TELEBOT_MIME_TYPE_DATA; + snprintf(mimes[index].data, sizeof(mimes[index].data), "%d", reply_to_message_id); + ++index; + } + + // Attach actual photo files using the correct names + for (int i = 0; i < count; ++i) + { + mimes[index].name = filenames[i]; // Use actual filename instead of generated name + mimes[index].type = TELEBOT_MIME_TYPE_FILE; + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", media_paths[i]); + ++index; + } + + // Perform request + telebot_error_e ret = telebot_core_curl_perform( + core_h, TELEBOT_METHOD_SEND_MEDIA_GROUP, mimes, index, response); + + // Clean up allocated filenames + for (int i = 0; i < count; i++) + { + free(filenames[i]); + } + free(filenames); + + // Clean up JSON object + json_object_put(media_array); + + return ret; +} + telebot_error_e telebot_core_send_location(telebot_core_handler_t *core_h, long long int chat_id, float latitude, float longitude, int live_period, bool disable_notification, int reply_to_message_id, const char *reply_markup, @@ -1780,7 +2030,7 @@ telebot_error_e telebot_core_restrict_chat_member(telebot_core_handler_t *core_h mimes[index].name = "can_pin_messages"; mimes[index].type = TELEBOT_MIME_TYPE_DATA; - snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (can_invite_users ? "true" : "false")); + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (can_pin_messages ? "true" : "false")); ++index; return telebot_core_curl_perform(core_h, TELEBOT_METHOD_RESTRICT_CHAT_MEMBER, mimes, index, response); @@ -1951,7 +2201,7 @@ telebot_error_e telebot_core_set_chat_permissions(telebot_core_handler_t *core_h mimes[index].name = "can_pin_messages"; mimes[index].type = TELEBOT_MIME_TYPE_DATA; - snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (can_invite_users ? "true" : "false")); + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (can_pin_messages ? "true" : "false")); ++index; return telebot_core_curl_perform(core_h, TELEBOT_METHOD_SET_CHAT_PERMISSIONS, mimes, index, response); @@ -2113,9 +2363,9 @@ telebot_error_e telebot_core_pin_chat_message(telebot_core_handler_t *core_h, snprintf(mimes[index].data, sizeof(mimes[index].data), "%d", message_id); ++index; - mimes[index].name = "message_id"; + mimes[index].name = "disable_notification"; mimes[index].type = TELEBOT_MIME_TYPE_DATA; - snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (disable_notification ? "True" : "False")); + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (disable_notification ? "true" : "false")); ++index; return telebot_core_curl_perform(core_h, TELEBOT_METHOD_PIN_CHAT_MESSAGE, mimes, index, response); @@ -2324,7 +2574,7 @@ telebot_error_e telebot_core_answer_callback_query(telebot_core_handler_t *core_ mimes[index].name = "show_alert"; mimes[index].type = TELEBOT_MIME_TYPE_DATA; - snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (show_alert ? "True" : "False")); + snprintf(mimes[index].data, sizeof(mimes[index].data), "%s", (show_alert ? "true" : "false")); ++index; if (url != NULL) diff --git a/src/telebot-parser.c b/src/telebot-parser.c index 310d2f8..49cbb86 100644 --- a/src/telebot-parser.c +++ b/src/telebot-parser.c @@ -31,7 +31,7 @@ static const char *telebot_update_type_str[TELEBOT_UPDATE_TYPE_MAX] = { "message", "edited_message", "channel_post", "edited_channel_post", "inline_query", - "chonse_inline_result", "callback_query", + "chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer"}; @@ -127,7 +127,7 @@ telebot_error_e telebot_parser_get_updates(struct json_object *obj, telebot_upda struct json_object *poll_answer = NULL; if (json_object_object_get_ex(item, "poll_answer", &poll_answer)) { - if (telebot_parser_get_poll_answer(poll_answer, &(result[index].poll_anser)) != TELEBOT_ERROR_NONE) + if (telebot_parser_get_poll_answer(poll_answer, &(result[index].poll_answer)) != TELEBOT_ERROR_NONE) ERR("Failed to parse poll answer of bot update"); result[index].update_type = TELEBOT_UPDATE_TYPE_POLL_ANSWER; continue; @@ -1586,7 +1586,7 @@ telebot_error_e telebot_parser_get_dice(struct json_object *obj, telebot_dice_t if ((obj == NULL) || (dice == NULL)) return TELEBOT_ERROR_INVALID_PARAMETER; - memset(dice, 0, sizeof(telebot_location_t)); + memset(dice, 0, sizeof(telebot_dice_t)); struct json_object *value = NULL; if (!json_object_object_get_ex(obj, "value", &value)) { diff --git a/src/telebot.c b/src/telebot.c index 62e8221..602c99c 100644 --- a/src/telebot.c +++ b/src/telebot.c @@ -244,7 +244,7 @@ telebot_error_e telebot_put_updates(telebot_update_t *updates, int count) telebot_put_poll(&(updates[index].poll)); break; case TELEBOT_UPDATE_TYPE_POLL_ANSWER: - telebot_put_poll_answer(&(updates[index].poll_anser)); + telebot_put_poll_answer(&(updates[index].poll_answer)); default: ERR("Unsupported update type: %d", updates[index].update_type); } @@ -606,6 +606,35 @@ telebot_error_e telebot_send_video_note(telebot_handler_t handle, long long int return ret; } +telebot_error_e telebot_send_media_group( + telebot_handler_t handle, + long long int chat_id, + char *media_paths[], + int count, + bool disable_notification, + int reply_to_message_id) +{ + telebot_hdata_t *_handle = (telebot_hdata_t *)handle; + if (_handle == NULL) + return TELEBOT_ERROR_NOT_SUPPORTED; + + if ((media_paths == NULL) || (count < 2) || (count > 10)) + return TELEBOT_ERROR_INVALID_PARAMETER; + + telebot_core_response_t response; + telebot_error_e ret = telebot_core_send_media_group( + _handle->core_h, + chat_id, + media_paths, + count, + disable_notification, + reply_to_message_id, + &response); + + telebot_core_put_response(&response); + return ret; +} + telebot_error_e telebot_send_location(telebot_handler_t handle, long long int chat_id, float latitude, float longitude, int live_period, bool disable_notification, int reply_to_message_id, const char *reply_markup) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0d599b5..ffec5cc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,4 +3,10 @@ SET(TEST_SRC echobot.c) ADD_EXECUTABLE(${TEST_NAME} ${TEST_SRC}) TARGET_LINK_LIBRARIES(${TEST_NAME} ${PKGS_LDFLAGS} ${PROJECT_NAME} pthread) +# Build estgb utility +SET(TEST_NAME estgb) +SET(TEST_SRC estgb.c) +ADD_EXECUTABLE(${TEST_NAME} ${TEST_SRC}) +TARGET_LINK_LIBRARIES(${TEST_NAME} ${PKGS_LDFLAGS} ${PROJECT_NAME}) + #EOF diff --git a/test/estgb.c b/test/estgb.c new file mode 100644 index 0000000..7194ea3 --- /dev/null +++ b/test/estgb.c @@ -0,0 +1,832 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0])) +#define TOKEN_SIZE 128 +#define FILENAME_TOKEN ".token" +#define FILENAME_USERID ".userid" +#define PID_FILE "/var/tmp/estgb_lock.pid" + +typedef void (*sendfunc_t)(char *filename); +typedef void (*sendfuncgroup_t)(char **filenames, int count); + +struct { + char *token; + long long user_id; + char *imgfile; + char *videofile; + char *docfile; + char *audiofile; + char *comment; + char *path; + char *text; + char *mask; + char *proxy_addr; + char *proxy_auth; + int isAnimation; + int isDaemonize; + int isRemove; + int isWeakConfig; + int isWildcard; + int isSingleton; + int isScan; + int needUnescape; + int useFileConfig; + int isPicsMediagroup; + int timeRescan; + int fd; + telebot_handler_t handle; + telebot_error_e ret; +} estgbconf; + +char* concatFilename(char *path, char *filename) { + int isSlash = 2; + char *result = NULL; + size_t path_len = strlen(path); + + if (path_len > 0 && path[path_len - 1] == '\\') + isSlash = 1; + + if (isSlash == 2) + asprintf(&result, "%s/%s", path, filename); + else + asprintf(&result, "%s%s", path, filename); + + return result; +} + +int checkSingleton() { + estgbconf.fd = open(PID_FILE, O_CREAT | O_RDWR, 0666); + if (estgbconf.fd == -1) + return 2; + + int rc = flock(estgbconf.fd, LOCK_EX | LOCK_NB); + if (rc) { + if (EWOULDBLOCK == errno) + return 1; + } + return 0; +} + +int checkMask(const struct dirent *dp) { + return (fnmatch(estgbconf.mask, dp->d_name, 0) == 0); +} + +void processWildcard_s(char *path, sendfunc_t sendfunc) { + struct dirent **namelist; + int n; + char *fullname; + + n = scandir(path, &namelist, checkMask, alphasort); + if (n < 0) + printf("Scandir error\n"); + else { + + while (n--) { + fullname = concatFilename(path, namelist[n]->d_name); + sendfunc(fullname); + free(fullname); + free(namelist[n]); + } + free(namelist); + } +} + +void processWildcard_group(char *path, sendfuncgroup_t sendfunc) { + struct dirent **namelist; + int n, j; + char **sendlist; + + n = scandir(path, &namelist, checkMask, alphasort); + if (n < 0) + printf("Scandir error\n"); + else { + + sendlist = calloc(n, sizeof(char*)); + + j = n; + while (n--) { + sendlist[n] = concatFilename(path, namelist[n]->d_name); + } + sendfunc(sendlist, j); + while (j--) { + free(namelist[j]); + free(sendlist[j]); + } + free(namelist); + free(sendlist); + } +} + +// Code of this function grabbed from source code of 'echo' utility +// https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/echo.c +inline static int hex2bin(unsigned char c) { + switch (c) { + default: + return c - '0'; + case 'a': + case 'A': + return 10; + case 'b': + case 'B': + return 11; + case 'c': + case 'C': + return 12; + case 'd': + case 'D': + return 13; + case 'e': + case 'E': + return 14; + case 'f': + case 'F': + return 15; + } +} + +// Zero-copy unescape function +// partially based on canonical 'echo' utility source code +// https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/echo.c +char* zc_unescape(char *input) { + unsigned char c; + char *p, *s; + + if (input == NULL) + return NULL; + + s = input; + p = input; + + while ((c = *s++)) { + if (c == '\\' && *s) { + switch (c = *s++) { + case 'e': + c = '\x1B'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = '\v'; + break; + case 'x': { + unsigned char ch = *s; + if (!isxdigit(ch)) + goto not_an_escape; + s++; + c = hex2bin(ch); + ch = *s; + if (isxdigit(ch)) { + s++; + c = c * 16 + hex2bin(ch); + } + } + break; + case '0': + c = 0; + if (!('0' <= *s && *s <= '7')) + break; + c = *s++; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + c -= '0'; + if ('0' <= *s && *s <= '7') + c = c * 8 + (*s++ - '0'); + if ('0' <= *s && *s <= '7') + c = c * 8 + (*s++ - '0'); + break; + case '\\': + break; + + not_an_escape: default: + *p = c; + p++; + break; + } + } + *p = c; + p++; + } + *p = '\0'; + + return input; +} + +void printHelp(void) { + printf( + "-----------------------------------------------------------------------------\n" + "| estgb :: enhanced sender telegram bot v1.4.0|\n" + "-----------------------------------------------------------------------------\n" + "\n" + "This telegram bot sends text, pictures, video, audio and documents (files)\n" + "according to command line paramaters\n" + "(c) 2018-2026 Flangeneer, Saint-Petersburg, Russia\n" + "\n" + "Usage: estgb [options]\n" + "\n" + "Commands:\n" + "--sendtext Send text\n" + "--sendpic Send picture\n" + "--senddoc Send document\n" + "--sendvideo Send video\n" + "--sendaudio Send audio\n" + "\n" + "Options for Telegram bot configuration (required):\n" + "--token Bot token\n" + "--userid User ID\n" + " or use:\n" + "--fileconfigs Read bot token and userid from files %s and %s accordingly\n" + "--path Working path for --fileconfigs option\n" + "\n" + "to use '--fileconfigs' there are should be two files:\n" + "1) %s - text file where first string is telegram bot token\n" + "2) %s - text file where first string is user ID\n" + "\n" + "Options to proceed the text and media:\n" + "--animation Send video as animation (GIF or H.264/MPEG-4 w/o sound mp4)\n" + "--comment Comment for picture/audio/video\n" + "--escape-seq Process escape sequences (emoji!) for 'comment' and 'text' (C-style)\n" + "--mediagroup Send multiple pictures as media group (when 2-10 items are exist)\n" + "\n" + "Options for network:\n" + "--proxy Use libcurl proxy. Examples: socks5://addr.org:8564 or http://addr.org:8564\n" + "--proxyauth Specify username and password for proxy\n" + "\n" + "Options for file processing:\n" + "--wildcard Process as wildcard instead of single file\n" + "--remove Remove(!) file after use for --sendpic, --sendvideo, --sendaudio, --senddoc\n" + "--force-remove Force remove(!) file after use even(!) error occured while send operation\n" + "\n" + "Options other:\n" + "--repeat-send Repeat send procedure N rounds. Specify 0 (zero) to infinite\n" + "--time-sleep Time to sleep betwen cycles in seconds. Default 1 sec\n" + "--singleton If another instance of bot is working, do nothing and exit\n" + "--daemon Daemonize process (process work in background)\n" + "--weakconfig [FOR DEBUG] Simplified command line parameters pre-check\n" + "\n", FILENAME_TOKEN, FILENAME_USERID, FILENAME_TOKEN, + FILENAME_USERID); + +} + +void initConfig(void) { + memset(&estgbconf, 0, sizeof(estgbconf)); + estgbconf.timeRescan = 1; + estgbconf.isScan = -1; +} + +void freeConfig(void) { + telebot_destroy(estgbconf.handle); + free(estgbconf.token); + free(estgbconf.text); + free(estgbconf.imgfile); + free(estgbconf.videofile); + free(estgbconf.audiofile); + free(estgbconf.docfile); + free(estgbconf.path); + free(estgbconf.comment); + free(estgbconf.proxy_addr); + free(estgbconf.proxy_auth); + close(estgbconf.fd); +} + +void printConfig(void) { + printf("Current config:\n" + "Bot token...............%s\n" + "User ID.................%lld\n" + "Image filename..........%s\n" + "Video filename..........%s\n" + "Audio filename..........%s\n" + "Doc filename............%s\n" + "Video as animation......%d\n" + "Send >2 pics as group...%d\n" + "Comment.................%s\n" + "Parse escape sequences..%d\n" + "Text to send............%s\n" + "Remove file after use...%d\n" + "Use file config.........%d\n" + "Working path............%s\n" + "Use wildcards...........%d\n" + "Use weak config.........%d\n" + "Run as singleton........%d\n" + "Work as daemon..........%d\n" + "Proxy...................%s\n" + "Proxy authentication....%s\n" + "Send rounds.............%d\n" + "Time sleep btw rounds...%d\n" + "\n", estgbconf.token, estgbconf.user_id, estgbconf.imgfile, + estgbconf.videofile, estgbconf.audiofile, estgbconf.docfile, + estgbconf.isAnimation, estgbconf.isPicsMediagroup, + estgbconf.comment, estgbconf.needUnescape, estgbconf.text, + estgbconf.isRemove, estgbconf.useFileConfig, estgbconf.path, + estgbconf.isWildcard, estgbconf.isWeakConfig, estgbconf.isSingleton, + estgbconf.isDaemonize, estgbconf.proxy_addr, estgbconf.proxy_auth, + estgbconf.isScan, estgbconf.timeRescan); +} + +void sendText(void) { + if (estgbconf.text != NULL) { + estgbconf.ret = telebot_send_message(estgbconf.handle, + estgbconf.user_id, estgbconf.text, "", false, false, 0, ""); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send text message: %d \n", estgbconf.ret); + } + } +} + +void sendPicture(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_photo( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + estgbconf.comment, + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send picture: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void removeFileGroup(char **filenames, int count) { + int j = count; + while (j--) + remove(filenames[j]); +} + +void sendGroup(char **filenames, int count) { + estgbconf.ret = telebot_send_media_group( + estgbconf.handle, + estgbconf.user_id, + filenames, + count, + false, // disable_notification + 0 // reply_to_message_id + // ← NO reply_markup (not supported by Telegram API for media group) + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send media group: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + removeFileGroup(filenames, count); + if (estgbconf.isRemove == 2) + removeFileGroup(filenames, count); +} + +void sendPicturesGroup(char **filenames, int count) { + + int i, j = 0; + + if (count == 1) { + sendPicture(filenames[0]); + return; + } + + char **sendlist; + sendlist = calloc(10, sizeof(char*)); + + for (i = 0; i < count; i++) { + + if (!access(filenames[i], R_OK)) { + sendlist[j] = strdup(filenames[i]); + j++; + } + + if (j == 10) { + sendGroup(sendlist, j); + while (j--) + free(sendlist[j]); + + j = 0; + } + } + + if (j > 0) { + sendGroup(sendlist, j); + while (j--) + free(sendlist[j]); + } + free(sendlist); +} + +void sendVideo(char *filename) { + if (!access(filename, R_OK)) { + if (estgbconf.isAnimation) + estgbconf.ret = telebot_send_animation( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + 0, 0, 0, // duration, width, height + NULL, // thumb + estgbconf.comment, + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + else + estgbconf.ret = telebot_send_video( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + 0, 0, 0, // duration, width, height + NULL, // thumb + estgbconf.comment, + NULL, // parse_mode + false, // supports_streaming + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send video: %d \n", estgbconf.ret); + + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendAudio(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_audio( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + estgbconf.comment, // caption + NULL, // parse_mode + 0, // duration + NULL, // performer + NULL, // title + NULL, // thumb + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send audio: %d \n", estgbconf.ret); + + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendDocument(char *filename) { + if (!access(filename, R_OK)) { + estgbconf.ret = telebot_send_document( + estgbconf.handle, + estgbconf.user_id, + filename, + true, + NULL, // thumb + estgbconf.comment, // caption + NULL, // parse_mode + false, // disable_notification + 0, // reply_to_message_id + NULL // reply_markup + ); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Failed to send document: %d \n", estgbconf.ret); + } else if (estgbconf.isRemove == 1) + remove(filename); + if (estgbconf.isRemove == 2) + remove(filename); + } +} + +void sendSingle(void) { + sendText(); + if (estgbconf.imgfile) + sendPicture(estgbconf.imgfile); + if (estgbconf.videofile) + sendVideo(estgbconf.videofile); + if (estgbconf.audiofile) + sendAudio(estgbconf.audiofile); + if (estgbconf.docfile) + sendDocument(estgbconf.docfile); +} + +void sendMultiple(void) { + + char *imgmask = NULL, *imgdir = NULL; + char *vidmask = NULL, *viddir = NULL; + char *audiomask = NULL, *auddir = NULL; + char *docmask = NULL, *docdir = NULL; + + sendText(); + + if (estgbconf.imgfile) { + imgmask = strdup(basename(estgbconf.imgfile)); + imgdir = strdup(estgbconf.imgfile); + imgdir = dirname(imgdir); + estgbconf.mask = imgmask; + if (estgbconf.isPicsMediagroup) + processWildcard_group(imgdir, sendPicturesGroup); + else processWildcard_s(imgdir, sendPicture); + } + + if (estgbconf.videofile) { + vidmask = strdup(basename(estgbconf.videofile)); + viddir = strdup(estgbconf.videofile); + viddir = dirname(viddir); + estgbconf.mask = vidmask; + processWildcard_s(viddir, sendVideo); + } + + if (estgbconf.audiofile) { + audiomask = strdup(basename(estgbconf.audiofile)); + auddir = strdup(estgbconf.audiofile); + auddir = dirname(auddir); + estgbconf.mask = audiomask; + processWildcard_s(auddir, sendAudio); + } + + if (estgbconf.docfile) { + docmask = strdup(basename(estgbconf.docfile)); + docdir = strdup(estgbconf.docfile); + docdir = dirname(docdir); + estgbconf.mask = docmask; + processWildcard_s(docdir, sendDocument); + } + + free(imgmask); + free(vidmask); + free(audiomask); + free(docmask); + free(imgdir); + free(viddir); + free(auddir); + free(docdir); + estgbconf.mask = NULL; +} + +void scan(void) { + + int infinite = (estgbconf.isScan == 0); + if (estgbconf.isScan < 0) + return; + + if (!estgbconf.isWildcard) { + while (estgbconf.isScan-- || infinite) { + sendSingle(); + sleep(estgbconf.timeRescan); + } + return; + } + + while (estgbconf.isScan-- || infinite) { + sendMultiple(); + sleep(estgbconf.timeRescan); + } + return; +} + +int readFileConfig(void) { + + char *tokenfilename, *useridfilename; + + if (estgbconf.path == NULL) { + tokenfilename = strdup(FILENAME_TOKEN); + useridfilename = strdup(FILENAME_USERID); + } else { + tokenfilename = concatFilename(estgbconf.path, FILENAME_TOKEN); + useridfilename = concatFilename(estgbconf.path, FILENAME_USERID); + } + + FILE *fp = fopen(tokenfilename, "r"); + if (fp == NULL) { + printf("Failed to open %s file\n", FILENAME_TOKEN); + return -1; + } + + estgbconf.token = (char*) calloc(TOKEN_SIZE, sizeof(char)); + if (fscanf(fp, "%s", estgbconf.token) == 0) { + printf("Reading token failed\n"); + fclose(fp); + return -1; + } + fclose(fp); + + fp = fopen(useridfilename, "r"); + if (fp == NULL) { + printf("Failed to open %s file\n", FILENAME_USERID); + return -1; + } + + if (fscanf(fp, "%lld", &estgbconf.user_id) == 0) { + printf("Reading user ID failed\n"); + fclose(fp); + return -1; + } + + free(tokenfilename); + free(useridfilename); + fclose(fp); + return 0; +} + +int checkConfig(void) { + if (estgbconf.token == NULL) + return 0; + if (estgbconf.user_id == 0) + return 0; + if ((estgbconf.imgfile == NULL) && (estgbconf.text == NULL) + && (estgbconf.videofile == NULL) && (estgbconf.docfile == NULL) + && (estgbconf.audiofile == NULL)) + return 0; + + if (!estgbconf.isWeakConfig && (estgbconf.imgfile == NULL) + && (estgbconf.videofile == NULL) && (estgbconf.docfile == NULL) + && (estgbconf.audiofile == NULL) && (estgbconf.isRemove != 0)) + return 0; + return 1; +} + +int globalInit(void) { + if (estgbconf.useFileConfig) { + if (readFileConfig() != 0) { + printf( + "Warning! Failed to read config files. Checking for command line config...\n"); + } + } + + if (!checkConfig()) { + printHelp(); + printConfig(); + printf("Configuration error. Nothing to do.\n\n"); + return 0; + } + + if (telebot_create(&estgbconf.handle, estgbconf.token) + != TELEBOT_ERROR_NONE) { + printf("Error. Telebot create failed (bad token?)\n\n"); + return 0; + } + + if (estgbconf.proxy_addr != NULL) { + estgbconf.ret = telebot_set_proxy(estgbconf.handle, + estgbconf.proxy_addr, estgbconf.proxy_auth); + if (estgbconf.ret != TELEBOT_ERROR_NONE) { + printf("Warning! Failed to init proxy: %d \n", estgbconf.ret); + } + } + + return 1; +} + +int parseCmdLine(int argc, char *argv[]) { + int j, more; + + if (argc < 3) { + printHelp(); + return 0; + } + + for (j = 1; j < argc; j++) { + more = ((j + 1) < argc); + if (!strcmp(argv[j], "--remove")) { + estgbconf.isRemove = 1; + } else if (!strcmp(argv[j], "--force-remove")) { + estgbconf.isRemove = 2; + } else if (!strcmp(argv[j], "--escape-seq")) { + estgbconf.needUnescape = 1; + } else if (!strcmp(argv[j], "--singleton")) { + estgbconf.isSingleton = 1; + } else if (!strcmp(argv[j], "--daemon")) { + estgbconf.isDaemonize = 1; + } else if (!strcmp(argv[j], "--mediagroup")) { + estgbconf.isPicsMediagroup = 1; + } else if (!strcmp(argv[j], "--repeat-send") && more) { + estgbconf.isScan = atoi(argv[++j]); + } else if (!strcmp(argv[j], "--time-sleep") && more) { + estgbconf.timeRescan = atoi(argv[++j]); + } else if (!strcmp(argv[j], "--animation")) { + estgbconf.isAnimation = 1; + } else if (!strcmp(argv[j], "--wildcard")) { + estgbconf.isWildcard = 1; + } else if (!strcmp(argv[j], "--weakconfig")) { + estgbconf.isWeakConfig = 1; + } else if (!strcmp(argv[j], "--sendpic") && more) { + estgbconf.imgfile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendvideo") && more) { + estgbconf.videofile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendtext") && more) { + estgbconf.text = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--senddoc") && more) { + estgbconf.docfile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--sendaudio") && more) { + estgbconf.audiofile = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--fileconfigs")) { + estgbconf.useFileConfig = 1; + } else if (!strcmp(argv[j], "--token") && more) { + estgbconf.token = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--userid") && more) { + estgbconf.user_id = atoll(argv[++j]); + } else if (!strcmp(argv[j], "--path") && more) { + estgbconf.path = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--comment") && more) { + estgbconf.comment = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--proxy") && more) { + estgbconf.proxy_addr = strdup(argv[++j]); + } else if (!strcmp(argv[j], "--proxyauth") && more) { + estgbconf.proxy_auth = strdup(argv[++j]); + } + + else if (!strcmp(argv[j], "--help")) { + printHelp(); + return 0; + } else { + printHelp(); + printConfig(); + printf("Unknown or not enough arguments for option '%s'.\n\n", + argv[j]); + return 0; + } + } + return 1; +} + +int main(int argc, char *argv[]) { + + initConfig(); + if (!parseCmdLine(argc, argv)) + goto exit; + + if (estgbconf.isSingleton) { + switch (checkSingleton()) { + case 0: + break; // No another instance found, continue to work + case 1: + goto exit; + // Found another instance of bot, exiting + case 2: + printf("Error open PID file for singleton check\n"); + goto exit; + // Something goes wrong, exiting + } + + } + + if (!globalInit()) + goto exit; + + if (estgbconf.isDaemonize) + daemon(0, 0); + + if (estgbconf.needUnescape) { + estgbconf.text = zc_unescape(estgbconf.text); + estgbconf.comment = zc_unescape(estgbconf.comment); + } + + if (estgbconf.isScan >= 0) { + scan(); + goto exit; + } + + if (estgbconf.isWildcard) + sendMultiple(); + else + sendSingle(); + + exit: freeConfig(); + return 0; +}