From 318a1ef88bb5ea09ff4cf953908aef5c76735a46 Mon Sep 17 00:00:00 2001 From: Trumeet Date: Fri, 1 Apr 2022 21:13:31 -0700 Subject: First Commit --- .gitignore | 76 ++++++ .gitmodules | 3 + .idea/.gitignore | 8 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/ksyxbot.iml | 2 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 7 + CMakeLists.txt | 20 ++ arch/.gitignore | 5 + arch/PKGBUILD | 73 ++++++ arch/botd.conf | 3 + arch/ksyxbotd.service | 13 + arch/sysusers.conf | 1 + arch/tmpfiles.conf | 2 + botd.h | 29 +++ cmdline.c | 91 +++++++ db.c | 124 +++++++++ db.h | 22 ++ log.c | 44 ++++ log.h | 50 ++++ logic.h | 15 ++ main.c | 50 ++++ query.c | 155 +++++++++++ refresh.c | 121 +++++++++ sql/0_1.sql | 10 + store.c | 125 +++++++++ td | 1 + tdutils.c | 368 +++++++++++++++++++++++++++ tdutils.h | 28 ++ 30 files changed, 1464 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/ksyxbot.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 arch/.gitignore create mode 100644 arch/PKGBUILD create mode 100644 arch/botd.conf create mode 100644 arch/ksyxbotd.service create mode 100644 arch/sysusers.conf create mode 100644 arch/tmpfiles.conf create mode 100644 botd.h create mode 100644 cmdline.c create mode 100644 db.c create mode 100644 db.h create mode 100644 log.c create mode 100644 log.h create mode 100644 logic.h create mode 100644 main.c create mode 100644 query.c create mode 100644 refresh.c create mode 100644 sql/0_1.sql create mode 100644 store.c create mode 160000 td create mode 100644 tdutils.c create mode 100644 tdutils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f846957 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +cmake-build-debug/ +cmake-build-release/ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f1273f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "td"] + path = td + url = https://github.com/tdlib/td.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/ksyxbot.iml b/.idea/ksyxbot.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/ksyxbot.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..79b3c94 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aa174e2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..7dc97e3 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7251ace --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.21) +project(ksyxbot VERSION 1.0 LANGUAGES C CXX) + +set(CMAKE_C_STANDARD 99) + +IF (CMAKE_BUILD_TYPE MATCHES Debug) + add_compile_definitions(TD_USE_ASAN) +ENDIF (CMAKE_BUILD_TYPE MATCHES Debug) + +add_subdirectory(td) + +set(CMAKE_C_FLAGS_DEBUG + "${CMAKE_C_FLAGS_DEBUG} -g3 -O0 -fsanitize=address") +set(CMAKE_EXE_LINKER_FLAGS_DEBUG + "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=address") +add_definitions(-D_POSIX_C_SOURCE=200809L) + +add_executable(ksyxbotd main.c tdutils.c tdutils.h log.c log.h botd.h db.c db.h cmdline.c store.c logic.h query.c refresh.c) +target_include_directories(ksyxbotd PUBLIC "${PROJECT_BINARY_DIR}") +target_link_libraries(ksyxbotd PRIVATE tdc tdsqlite m) diff --git a/arch/.gitignore b/arch/.gitignore new file mode 100644 index 0000000..8991b67 --- /dev/null +++ b/arch/.gitignore @@ -0,0 +1,5 @@ +*.zst +pkg/ +src/ +td/ +ksyxbot/ diff --git a/arch/PKGBUILD b/arch/PKGBUILD new file mode 100644 index 0000000..9918a4f --- /dev/null +++ b/arch/PKGBUILD @@ -0,0 +1,73 @@ +# Maintainer: Yuuta Liang +pkgname=ksyxbot-git +pkgver=r11.546381f +pkgrel=1 +pkgdesc="ksyx ksyx ksyx" +arch=(x86_64) +url="https://git.yuuta.moe/ksyxbot.git" +license=('custom') +groups=() +depends=(zlib openssl) +makedepends=('git') +provides=("${pkgname%-git}") +conflicts=("${pkgname%-git}") +replaces=() +backup=(etc/ksyxbot/botd.conf) +options=() +install= +source=('ksyxbot::git+https://git.yuuta.moe/ksyxbot.git' +'td::git+https://github.com/tdlib/td.git' +'ksyxbotd.service' +'sysusers.conf' +'tmpfiles.conf' +'botd.conf') +noextract=() +md5sums=('SKIP' + 'SKIP' + '36f708d68d52bac366a333628a1213a7' + '9d905a018be08f0ac09796e301949afd' + 'b47ae2adc14a370080941b535843b095' + '062bd08b22c915956e2fe655204c9427') + +pkgver() { + cd "$srcdir/${pkgname%-git}" + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +prepare() { + cd "$srcdir/${pkgname%-git}" + git submodule init + git config submodule.td.url $srcdir/td + git submodule update +} + +build() { + cd "$srcdir/${pkgname%-git}" + mkdir -p cmake-build-release + cd cmake-build-release + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH="$pkgdir/usr" .. + make ksyxbotd +} + +check() { + cd "$srcdir/${pkgname%-git}" +} + +package() { + cd "$srcdir/${pkgname%-git}" + mkdir -p "$pkgdir/etc/ksyxbot/" + chmod 700 "$pkgdir/etc/ksyxbot/" + install -Dm700 "$srcdir/default.conf" "$pkgdir/etc/ksyxbot/default.conf" + mkdir -p "$pkgdir/usr/lib/sysusers.d/" + install -Dm644 "$srcdir/sysusers.conf" "$pkgdir/usr/lib/sysusers.d/ksyxbot.conf" + mkdir -p "$pkgdir/var/lib/ksyxbot/" + chmod 700 "$pkgdir/var/lib/ksyxbot/" + mkdir -p "$pkgdir/usr/lib/tmpfiles.d/" + install -Dm644 "$srcdir/tmpfiles.conf" "$pkgdir/usr/lib/tmpfiles.d/ksyxbot.conf" + mkdir -p "$pkgdir/usr/lib/systemd/system/" + install -Dm644 "$srcdir/ksyxbotd.service" "$pkgdir/usr/lib/systemd/system/ksyxbotd.service" + mkdir -p "$pkgdir/usr/share/licenses/${pkgname%-git}/" + install -Dm644 "./LICENSE" "$pkgdir/usr/share/licenses/${pkgname%-git}/LICENSE" + mkdir -p "$pkgdir/usr/bin/" + install -Dm755 "./cmake-build-release/ksyxbotd" "$pkgdir/usr/bin/ksyxbotd" +} diff --git a/arch/botd.conf b/arch/botd.conf new file mode 100644 index 0000000..d1fbef4 --- /dev/null +++ b/arch/botd.conf @@ -0,0 +1,3 @@ +TD_API_ID= +TD_API_HASH= +BOT_TOKEN= diff --git a/arch/ksyxbotd.service b/arch/ksyxbotd.service new file mode 100644 index 0000000..c71fe66 --- /dev/null +++ b/arch/ksyxbotd.service @@ -0,0 +1,13 @@ +[Unit] +Description=ksyxbotd +Documentation=man:ksyxbotd(1) +After=network.target network-online.target nss-lookup.target + +[Service] +Type=simple +User=ksyxbot +EnvironmentFile=/etc/ksyxbot/botd.conf +ExecStart=/usr/bin/ksyxbotd --td_path /var/lib/ksyxbot/td/ --db_path /var/lib/ksyxbot/db.sql + +[Install] +WantedBy=multi-user.target diff --git a/arch/sysusers.conf b/arch/sysusers.conf new file mode 100644 index 0000000..652de4f --- /dev/null +++ b/arch/sysusers.conf @@ -0,0 +1 @@ +u ksyxbot - "ksyxbotd" /var/lib/ksyxbot/ /bin/nologin diff --git a/arch/tmpfiles.conf b/arch/tmpfiles.conf new file mode 100644 index 0000000..5702e68 --- /dev/null +++ b/arch/tmpfiles.conf @@ -0,0 +1,2 @@ +d /etc/ksyxbot/ 0700 ksyxbot nobody - +d /var/lib/ksyxbot/ 0700 ksyxbot nobody - diff --git a/botd.h b/botd.h new file mode 100644 index 0000000..5bdd8fe --- /dev/null +++ b/botd.h @@ -0,0 +1,29 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#ifndef KSYXBOT_BOTD_H +#define KSYXBOT_BOTD_H + +#include + +#define ADMIN 2122005901 +#define CHANNEL -1001794945713 + +extern int td; + +typedef struct cmd_s { + const char *bot_token; + int api_id; + const char *api_hash; + bool test_dc; + const char *td_path; + bool logout; + const char *db_path; +} cmd_t; + +extern cmd_t cmd; + +void parse_cmdline(int argc, char **argv); + +#endif /* KSYXBOT_BOTD_H */ diff --git a/cmdline.c b/cmdline.c new file mode 100644 index 0000000..778c7ae --- /dev/null +++ b/cmdline.c @@ -0,0 +1,91 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "botd.h" +#include "log.h" + +#include +#include +#include +#include +#include +#include + +static inline void parse_api_id(const char *str) { + char *endptr; + intmax_t num = strtoimax(str, &endptr, 10); + if (strcmp(endptr, "") != 0 || (num == INTMAX_MAX && errno == ERANGE) || + num > INT_MAX || num < INT_MIN) { + LOGFEV("Invalid API Hash: %s.", 64, str); + } + cmd.api_id = (int) num; +} + +void parse_cmdline(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (arg[0] != '-' || arg[1] != '-') + LOGFEV("Unexpected argument: %s.", 64, arg); + const char *a = &arg[2]; + if (!strcmp(a, "test")) { + LOGD("Test DC is in use."); + cmd.test_dc = true; + } else if (!strcmp(a, "logout")) { + cmd.logout = true; + } else if (!strcmp(a, "api_id")) { + if (i == (argc - 1)) LOGFEV("%s expects an argument.", 64, arg); + parse_api_id(argv[++i]); + } else if (!strcmp(a, "api_hash")) { + if (i == (argc - 1)) LOGFEV("%s expects an argument.", 64, arg); + cmd.api_hash = argv[++i]; + } else if (!strcmp(a, "bot_token")) { + if (i == (argc - 1)) LOGFEV("%s expects an argument.", 64, arg); + cmd.bot_token = argv[++i]; + } else if (!strcmp(a, "td_path")) { + if (i == (argc - 1)) LOGFEV("%s expects an argument.", 64, arg); + cmd.td_path = argv[++i]; + } else if (!strcmp(a, "db_path")) { + if (i == (argc - 1)) LOGFEV("%s expects an argument.", 64, arg); + cmd.db_path = argv[++i]; + } else if (!strcmp(a, "help")) { + printf("Usage: " + "[TD_API_ID=] " + "[TD_API_HASH=] " + "[TD_PATH=] " + "[BOT_TOKEN=] " + "[DB_PATH=] " + "%s " + "[--help] " + "[--logout] " + "[--test] " + "[--api_id TD_API_ID] " + "[--api_hash TD_API_HASH] " + "[--td_path TD_PATH] " + "[--bot_token BOT_TOKEN] " + "[--db_path DB_PATH] ", + argv[0]); + exit(0); + } else { + LOGFEV("Invalid command: %s. Use %s --help to view usage.", + 64, + arg, + argv[0]); + } + } + + if (!cmd.api_hash && !(cmd.api_hash = getenv("TD_API_HASH"))) + LOGFEV("Required environment variable %s is missing.", 64, "TD_API_HASH"); + if (!cmd.bot_token && !(cmd.bot_token = getenv("BOT_TOKEN"))) + LOGFEV("Required environment variable %s is missing.", 64, "BOT_TOKEN"); + if (!cmd.td_path && !(cmd.td_path = getenv("TD_PATH"))) + LOGFEV("Required environment variable %s is missing.", 64, "TD_PATH"); + if (!cmd.db_path && !(cmd.db_path = getenv("DB_PATH"))) + LOGFEV("Required environment variable %s is missing.", 64, "DB_PATH"); + if (!cmd.api_id) { + const char *a; + if (!(a = getenv("TD_API_ID"))) + LOGFEV("Required environment variable %s is missing.", 64, "TD_API_ID"); + parse_api_id(a); + } +} \ No newline at end of file diff --git a/db.c b/db.c new file mode 100644 index 0000000..ff9388c --- /dev/null +++ b/db.c @@ -0,0 +1,124 @@ +// +// Created by yuuta on 11/28/21. +// + +#include "db.h" +#include "log.h" +#include "botd.h" + +#include +#include +#include +#include +#include +#include + +#ifndef SQL_PATH +#define SQL_PATH "../sql/" +#endif + +sqlite3 *db = NULL; + +void db_init(void) { + int r; + r = sqlite3_open(cmd.db_path, &db); + if (r) { + LOGFEV("Cannot open SQLite3 database: %s", r, sqlite3_errstr(r)); + } + char *errmsg = NULL; + /* Enable foreign key support */ + r = sqlite3_exec(db, "PRAGMA foreign_keys = ON;", NULL, NULL, &errmsg); + if (r) { + goto sql_err; + sql_err: + db_close(); + LOGFEV("%s", r, errmsg); + } + /* Get user_version */ + sqlite3_stmt *stmt; + r = sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, 0); + if (r) { + goto sql_err; + } + r = sqlite3_step(stmt); + if (r != SQLITE_ROW) { + sqlite3_finalize(stmt); + goto sql_err; + } + const int current_var = sqlite3_column_int(stmt, 0); + r = sqlite3_finalize(stmt); + /* Upgrade one by one */ + FILE *fd_sql = NULL; + char *path_sql = NULL; + const unsigned int path_len = strlen(SQL_PATH) + 20; + path_sql = calloc(path_len, sizeof(char)); + if (path_sql == NULL) { + r = errno; + db_close(); + LOGFEV("Cannot allocate memory: %s", r, strerror(r)); + } + for (int i = current_var; true; i++) { + snprintf(path_sql, path_len - 1, "%s/%d_%d.sql", SQL_PATH, i, i + 1); + fd_sql = fopen(path_sql, "r"); + if (fd_sql == NULL) { + if (errno != ENOENT) { + r = errno; + free(path_sql); + db_close(); + LOGFEV("Cannot upgrade database to %d: %s", r, i + 1, strerror(r)); + } + break; + } + char *sql_str = NULL; + char *line = NULL; + size_t len = 0; + ssize_t tread = 0; + ssize_t nread; + while ((nread = getline(&line, &len, fd_sql)) != -1) { + tread += nread; + if (sql_str == NULL) { + sql_str = calloc(nread + 1, sizeof(char)); + if (sql_str == NULL) { + r = errno; + fclose(fd_sql); + free(line); + free(path_sql); + db_close(); + LOGFEV("Cannot allocate memory: %s", r, strerror(r)); + } + strcpy(sql_str, line); + } else { + char *newptr = realloc(sql_str, (tread + 1) * sizeof(char)); + if (newptr == NULL) { + r = errno; + fclose(fd_sql); + free(line); + free(sql_str); + free(path_sql); + db_close(); + LOGFEV("Cannot allocate memory: %s", r, strerror(r)); + } + sql_str = newptr; + strcat(sql_str, line); + } + } + fclose(fd_sql); + free(line); + r = sqlite3_exec(db, sql_str, NULL, NULL, &errmsg); + if (r) { + free(sql_str); + db_close(); + LOGFV("%s", errmsg); + break; + } + free(sql_str); + } + free(path_sql); +} + +void db_close(void) { + if (db == NULL) + return; + sqlite3_close(db); + db = NULL; +} diff --git a/db.h b/db.h new file mode 100644 index 0000000..269cd70 --- /dev/null +++ b/db.h @@ -0,0 +1,22 @@ +// +// Created by yuuta on 11/28/21. +// + +#ifndef _DB_H +#define _DB_H + +#include +#include +#include +#include + +#define ULEN(X) (X == 0 ? 1 : (int)floor(log10(X)) + 1) +#define LLEN(X) ((int)floor(log10(llabs(X))) + 1 + ((X < 0) ? 1 : 0)) + +extern sqlite3 *db; + +void db_init(void); + +void db_close(void); + +#endif /* _DB_H */ diff --git a/log.c b/log.c new file mode 100644 index 0000000..f3fa7bb --- /dev/null +++ b/log.c @@ -0,0 +1,44 @@ +/* + * Created by yuuta on 1/1/22. + */ + +#include "log.h" + +#include +#include +#include + +void g_log(enum log_level level, + const char *file, + int line, + const char *format, + ...) { + FILE *stream = level <= log_warn ? stderr : stdout; + switch (level) { + case log_fetal: + fprintf(stream, "F"); + break; + case log_error: + fprintf(stream, "E"); + break; + case log_warn: + fprintf(stream, "W"); + break; + case log_info: + fprintf(stream, "I"); + break; + case log_debug: + fprintf(stream, "D"); + break; + default: + fprintf(stderr, "Unknown log level: %d.\n", level); + assert(0); + } + fprintf(stream, "[%s:%d]: ", + file, line); + va_list list; + va_start(list, format); + vfprintf(stream, format, list); + va_end(list); + fprintf(stream, "\n"); +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..cea79a6 --- /dev/null +++ b/log.h @@ -0,0 +1,50 @@ +/* + * Created by yuuta on 1/1/22. + */ + +#ifndef _LOG_H +#define _LOG_H + +enum log_level { + log_fetal = 1, + log_error = 2, + log_warn = 3, + log_info = 4, + log_debug = 5 +}; + +void g_log(enum log_level level, + const char *file, + int line, + const char *format, + ...); + +#define LOGFE(X, code) \ +do { g_log(log_fetal, __FUNCTION__, __LINE__, X); \ +exit(code); } while (0) + +#define LOGFEV(X, code, ...) \ +do { g_log(log_fetal, __FUNCTION__, __LINE__, X, __VA_ARGS__); \ +exit(code); } while (0) + +#define LOGF(X) g_log(log_fetal, __FUNCTION__, __LINE__, X) + +#define LOGFV(X, ...) g_log(log_fetal, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGE(X) g_log(log_error, __FUNCTION__, __LINE__, X) + +#define LOGEV(X, ...) g_log(log_error, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGW(X) g_log(log_warn, __FUNCTION__, __LINE__, X) + +#define LOGWV(X, ...) g_log(log_warn, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGI(X) g_log(log_info, __FUNCTION__, __LINE__, X) + +#define LOGIV(X, ...) g_log(log_info, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#define LOGD(X) g_log(log_debug, __FUNCTION__, __LINE__, X) + +#define LOGDV(X, ...) g_log(log_debug, __FUNCTION__, __LINE__, X, __VA_ARGS__) + +#endif /* _LOG_H */ diff --git a/logic.h b/logic.h new file mode 100644 index 0000000..d85a631 --- /dev/null +++ b/logic.h @@ -0,0 +1,15 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#ifndef KSYXBOT_LOGIC_H +#define KSYXBOT_LOGIC_H + +#include "td/td/telegram/td_c_client.h" +#include "tdutils.h" + +void store(struct TdMessage *msg); + +void refresh(struct TdMessage *msg); + +#endif /* KSYXBOT_LOGIC_H */ diff --git a/main.c b/main.c new file mode 100644 index 0000000..806c8ef --- /dev/null +++ b/main.c @@ -0,0 +1,50 @@ +#include "db.h" +#include "log.h" +#include "tdutils.h" +#include "botd.h" +#include "logic.h" + +#include +#include + +cmd_t cmd = { + NULL, + 0, + NULL, + false, + NULL, + false +}; + +int post_auth() { + LOGI("OK"); + return 0; +} + +static void ate(void) { + db_close(); + td_free(); +} + +int main(int argc, char **argv) { + atexit(&ate); + parse_cmdline(argc, argv); + db_init(); + td_init(); + td_loop(); + return 0; +} + +int handle_message(struct TdUpdateNewMessage *update) { + struct TdMessage *msg = update->message_; + if (msg->sender_id_->ID == CODE_MessageSenderUser && + ((struct TdMessageSenderUser *) msg->sender_id_)->user_id_ == ADMIN) { + if (msg->content_->ID == CODE_MessageText && + !strcmp(((struct TdMessageText *) msg->content_)->text_->text_, "/refresh")) { + refresh(msg); + return 0; + } + } + store(msg); + return 0; +} \ No newline at end of file diff --git a/query.c b/query.c new file mode 100644 index 0000000..41aa1e8 --- /dev/null +++ b/query.c @@ -0,0 +1,155 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "tdutils.h" +#include "logic.h" +#include "log.h" +#include "db.h" + +#include +#include +#include +#include +#include + +struct result { + char id[10]; + /* Title in selection */ + char *title; + /* Description in selection */ + char *description; + /* Text after sending */ + char *text; + char *url; +}; + +static void cb_answer(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + if (!successful) { + LOGEV("Cannot answer inline request: %s.", + error ? error->message_ : "(NULL)"); + } +} + +int handle_inline(struct TdUpdateNewInlineQuery *update) { + struct result results[10]; + memset(results, 0, sizeof(results)); + sqlite3_stmt *stmt; + int r; + if ((r = sqlite3_prepare_v2(db, + "SELECT id, t, url FROM says ORDER BY RANDOM() LIMIT 10;", + -1, + &stmt, + NULL))) { + goto sql_err; + sql_err: + snprintf(results[0].id, 10, "e%d", r); + char *msg = (char *) sqlite3_errstr(r); + LOGEV("Cannot query: %s.", msg); + results[0].title = "Error!"; + results[0].text = msg; + results[0].description = msg; + goto answer; + } + + int j = 0; + goto step; + step: + switch (r = sqlite3_step(stmt)) { + case SQLITE_ROW: { + char *t = (char *) sqlite3_column_text(stmt, 1); + char *url = (char *) sqlite3_column_text(stmt, 3); + + char *url1 = NULL; + char *title = NULL; + char *t1 = NULL; + + if (url && !(url1 = strdup(url))) { + LOGEV("Cannot allocate memory: %s.", strerror(errno)); + break; + } + if (!(title = strdup(t))) { + LOGEV("Cannot allocate memory: %s.", strerror(errno)); + break; + } + if (!(t1 = strdup(t))) { + LOGEV("Cannot allocate memory: %s.", strerror(errno)); + break; + } + results[j].title = title; + snprintf(results[j].id, 10, "%d", sqlite3_column_int(stmt, 0)); + results[j].text = t1; + results[j].url = url1; + results[j].description = url1; + if ((++j) > 9) break; + goto step; + } + case SQLITE_DONE: { + break; + } + default: { + goto sql_err; + } + } + + sqlite3_finalize(stmt); + + if (!j) { + results[0].title = "No data"; + results[0].description = "No data is available. Check it out later."; + sprintf(results[0].id, "e_nodat"); + results[0].text = "No data is available. Check it out later.\n\n" + "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; + } + assert(results[0].title); + goto answer; + answer: + { + struct TdInputInlineQueryResultArticle *results_inline[10]; /* No need to memset(). Var 'i' will assure that. */ + int i; + for (i = 0; i < sizeof(results) / sizeof(struct result); i++) { + struct result res = results[i]; + if (!res.title) { + break; + } + results_inline[i] = TdCreateObjectInputInlineQueryResultArticle( + res.id, + NULL, + false, + res.title, + res.description, + NULL, + 0, + 0, + NULL, + (struct TdInputMessageContent *) TdCreateObjectInputMessageText( + TdCreateObjectFormattedText(res.text, + (struct TdVectorTextEntity *) TdCreateObjectVectorObject(0, + NULL)), + true, + false + )); + } + td_send(TdCreateObjectAnswerInlineQuery(update->id_, + false, + (struct TdVectorInputInlineQueryResult *) + TdCreateObjectVectorObject(i, + (struct TdObject **) results_inline), + 0, + NULL, + NULL, + NULL), + &cb_answer, + NULL); + for (i = 0; i < sizeof(results) / sizeof(struct result); i++) { + struct result res = results[i]; + if (!res.title) { + break; + } + free(res.title); + if (res.url) free(res.url); + free(res.text); + } + return r; + } +} diff --git a/refresh.c b/refresh.c new file mode 100644 index 0000000..8146563 --- /dev/null +++ b/refresh.c @@ -0,0 +1,121 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "log.h" +#include "logic.h" +#include "botd.h" +#include "tdutils.h" + +#include +#include + +struct refresh_session { + bool running; + long long chat_id; + long long message_id; + long long offset_message_id; + unsigned int progress; +}; + +static struct refresh_session current_session = { + false, + 0, + 0, + 0 +}; + +static void refresh_get(void); + +static void send_msg(long long chat_id, long long message_id, char *text) { + td_send(TdCreateObjectSendMessage(chat_id, + 0, + message_id, + TdCreateObjectMessageSendOptions(false, + false, + true, + NULL), + NULL, + (struct TdInputMessageContent *) + TdCreateObjectInputMessageText( + TdCreateObjectFormattedText(text, + (struct TdVectorTextEntity *) + TdCreateObjectVectorObject( + 0, NULL) + ), + false, + false)), + NULL, + NULL); +} + +static void send_administrative_msg(char *text) { + send_msg(current_session.chat_id, current_session.message_id, text); +} + +static void cb_refresh_mlink(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + if (!successful) { + char msg[512]; + snprintf(msg, 512, "Cannot get message link info: %d (%s)", + error ? error->code_ : -1, + error ? error->message_ : "NULL"); + send_administrative_msg(msg); + current_session.running = false; + return; + } + struct TdMessageLinkInfo *info = (struct TdMessageLinkInfo *) result; + if (!info->message_) { + refresh_get(); + return; + } + LOGDV("%lld", info->message_->id_); + store(info->message_); + refresh_get(); +} + +static void cb_refresh_link(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + if (!successful) { + char msg[512]; + snprintf(msg, 512, "Cannot get link: %d (%s)", + error ? error->code_ : -1, + error ? error->message_ : "NULL"); + send_administrative_msg(msg); + current_session.running = false; + return; + } + struct TdInternalLinkType *type = (struct TdInternalLinkType *) result; + if (type->ID != CODE_InternalLinkTypeMessage) { + refresh_get(); + return; + } + struct TdInternalLinkTypeMessage *m = (struct TdInternalLinkTypeMessage *) type; + td_send(TdCreateObjectGetMessageLinkInfo(m->url_), &cb_refresh_mlink, NULL); +} + +static void refresh_get(void) { + if (current_session.progress > 815) { + send_administrative_msg("Done."); + current_session.running = false; + return; + } + char url[64]; + sprintf(url, "https://t.me/ksyxsays/%u", current_session.progress ++); + LOGDV("Get %s", url); + td_send(TdCreateObjectGetInternalLinkType(url), + &cb_refresh_link, + NULL); +} + +void refresh(struct TdMessage *msg) { + if (current_session.running) { + send_msg(msg->chat_id_, msg->id_, "A refresh is already in progress."); + return; + } + current_session.running = true; + current_session.message_id = msg->id_; + current_session.chat_id = msg->chat_id_; + current_session.offset_message_id = 0; + current_session.progress = 1; + send_administrative_msg("Refreshing ..."); + refresh_get(); +} \ No newline at end of file diff --git a/sql/0_1.sql b/sql/0_1.sql new file mode 100644 index 0000000..847b16d --- /dev/null +++ b/sql/0_1.sql @@ -0,0 +1,10 @@ +-- Init +CREATE TABLE "says" +( + "id" INTEGER PRIMARY KEY, + "t" TEXT NOT NULL UNIQUE, + "url" TEXT, + "msg" INTEGER NOT NULL +); + +PRAGMA user_version = 1; \ No newline at end of file diff --git a/store.c b/store.c new file mode 100644 index 0000000..f49f23b --- /dev/null +++ b/store.c @@ -0,0 +1,125 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "log.h" +#include "logic.h" +#include "db.h" +#include "botd.h" + +#include +#include +#include +#include + +/* Temp object between message callback and link callback */ +struct say { + long long msg_id; + long long chat_id; + char *text; +}; + +static void cb_link(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + struct say *s = cb_arg; + if (!successful) { + LOGEV("Cannot get link for %lld/%lld: %s (%d).", + s->chat_id, s->msg_id, + error ? error->message_ : "NULL", + error ? error->code_ : 0); + goto cleanup; + } + LOGDV("%s", s->text); + struct TdMessageLink *link = (struct TdMessageLink *) result; + int r; + sqlite3_stmt *stmt; + if ((r = sqlite3_prepare_v2(db, + "INSERT INTO says(t, url, msg) VALUES(?, ?, ?)", + -1, + &stmt, + NULL))) { + goto sql_err; + sql_err: + LOGEV("Cannot insert: %s.", sqlite3_errstr(r)); + goto cleanup; + } + if ((r = sqlite3_bind_text(stmt, 1, s->text, (int) strlen(s->text), NULL))) { + goto sql_err; + } + if ((r = sqlite3_bind_text(stmt, 2, link->link_, (int) strlen(link->link_), NULL))) { + goto sql_err; + } + if ((r = sqlite3_bind_int64(stmt, 3, s->msg_id))) { + goto sql_err; + } + r = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (r != SQLITE_DONE && r != SQLITE_CONSTRAINT) { + goto sql_err; + } + goto cleanup; + cleanup: + free(s->text); + free(s); +} + +static bool filter(struct TdMessage *msg) { + if (msg->chat_id_ != CHANNEL) + return false; + if (!msg->forward_info_ || !msg->forward_info_->origin_) { + return false; + } + struct TdMessageForwardOrigin *origin = msg->forward_info_->origin_; + switch (origin->ID) { + case CODE_MessageForwardOriginChannel: { + struct TdMessageForwardOriginChannel *chan = + (struct TdMessageForwardOriginChannel *) origin; + if (chan->chat_id_ != -1001304761546 && + chan->chat_id_ != -1001565681839) + return false; + break; + } + case CODE_MessageForwardOriginHiddenUser: { + struct TdMessageForwardOriginHiddenUser *hid_user = + (struct TdMessageForwardOriginHiddenUser *) origin; + if (strcmp("ksyx", hid_user->sender_name_) != 0) return false; + break; + } + default: { + return false; + } + } + + if (msg->content_->ID != CODE_MessageText) { + return false; + } + return true; +} + +void store(struct TdMessage *msg) { + if (!filter(msg)) return; + struct TdMessageText *text = (struct TdMessageText *) msg->content_; + const char *t = text->text_->text_; + struct say *s; + if (!(s = malloc(sizeof(struct say)))) { + LOGEV("Cannot allocate memory: %s.", strerror(errno)); + return; + } + memset(s, 0, sizeof(struct say)); + s->chat_id = msg->chat_id_; + s->msg_id = msg->id_; + /* I'm just too lazy to manually free in each callback, so just strdup. */ + if (!(s->text = strdup(t))) { + LOGEV("Cannot allocate memory: %s.", strerror(errno)); + free(s); + return; + } + td_send(TdCreateObjectGetMessageLink(msg->chat_id_, + msg->id_, + 0, + false, + false), + &cb_link, + s); + +} + diff --git a/td b/td new file mode 160000 index 0000000..461b740 --- /dev/null +++ b/td @@ -0,0 +1 @@ +Subproject commit 461b740987101972cce65d1d5c996f455e4891ee diff --git a/tdutils.c b/tdutils.c new file mode 100644 index 0000000..89fb3b2 --- /dev/null +++ b/tdutils.c @@ -0,0 +1,368 @@ +#include "tdutils.h" +#include "botd.h" +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct td_callback { + struct td_callback *next; + + long long request_id; + + void (*cb)(bool, struct TdObject *, struct TdError *, void *); + + void *cb_arg; +}; + +int td = -1; +long long my_id = -1; +static struct td_callback *cbs; + +/** + * Last request_id. Increased whenever sending a query. + * + * Write: any + * Read: any + */ +static atomic_llong last_req_id = 0; + +/** + * Set to true by main thread when received authorizationStateClosed or authorizationStateClosing. + * Stop accepting new queries. + * + * Write: main + * Read: any + */ +bool closing = false; + +static bool sighandler_setup = false; +static pthread_t thread_sighandler; + +/** + * Used for sigwait(2). + */ +sigset_t set; + +static void *main_sighandler(void *arg) { + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + int r; + int sig; + + while (true) { + r = sigwait(&set, &sig); + if (r) { + fprintf(stderr, "Cannot call sigwait(): %d.\n", r); + goto cleanup; + } + switch (sig) { + case SIGINT: + case SIGTERM: + if (td == -1) goto cleanup; + tg_close(); + goto cleanup; + default: + break; + } + } + cleanup: + pthread_exit(NULL); +} + +static void sighandler_init() { + int r; + sigemptyset(&set); + sigaddset(&set, SIGTERM); + sigaddset(&set, SIGINT); + sigaddset(&set, SIGUSR1); + if ((r = pthread_sigmask(SIG_BLOCK, &set, NULL))) { + LOGFEV("pthread_sigmask", r, strerror(r)); + } + if ((r = pthread_create(&thread_sighandler, NULL, &main_sighandler, NULL))) { + LOGFEV("pthread_create", r, strerror(r)); + } + sighandler_setup = true; +} + +static int sighandler_close() { + if (!sighandler_setup) return 0; + pthread_cancel(thread_sighandler); + int r = pthread_join(thread_sighandler, NULL); + if (!r) sighandler_setup = false; + return r; +} + +static int tdcb_push(long long request_id, void (*cb)(bool, struct TdObject *, struct TdError *, void *), void *cb_arg) { + struct td_callback *current_ptr = malloc(sizeof(struct td_callback)); + if (current_ptr == NULL) { + int r = errno; + fprintf(stderr, "Cannot allocate memory: %s\n", strerror(r)); + return r; + } + current_ptr->next = NULL; + current_ptr->request_id = request_id; + current_ptr->cb = cb; + current_ptr->cb_arg = cb_arg; + if (cbs == NULL) { + cbs = current_ptr; + } else { + current_ptr->next = cbs; + cbs = current_ptr; + } + return 0; +} + +int td_send(void *func, void (*cb)(bool, struct TdObject *, struct TdError *, void *), void *cb_arg) { + if (closing) { + TdDestroyObjectFunction((struct TdFunction *)func); + return 0; + } + if (last_req_id == LLONG_MAX) last_req_id = 0; + last_req_id++; + int r; + if (cb != NULL && (r = tdcb_push(last_req_id, cb, cb_arg))) { + return r; + } + TdCClientSend(td, (struct TdRequest) { + last_req_id, + (struct TdFunction *) func + }); + return 0; +} + +static int tdcb_call(long long request_id, bool successful, struct TdObject *result, struct TdError *error) { + if (cbs == NULL) return 0; + struct td_callback *current_ptr = cbs; + bool node_found = false; + while (current_ptr != NULL) { + if (current_ptr->request_id == request_id) { + node_found = true; + current_ptr->cb(successful, result, error, current_ptr->cb_arg); + if (error != NULL && + current_ptr->cb == &fetal_cb) { + /* The fetal_cb callback does not print anything */ + /* because it does not know the request_id. */ + fprintf(stderr, "Error: Request %lld: %s (%d)\n", + request_id, + error->message_, + error->code_); + } + break; + } + current_ptr = current_ptr->next; + } + if (node_found) { + /* + * The callback function may insert nodes to the link. + * Therefore, we do the iteration again after calling the function to + * delete the current one. + * Need a better implementation. + */ + current_ptr = cbs; + struct td_callback *prev_ptr = NULL; + while (current_ptr != NULL) { + if (current_ptr->request_id == request_id) { + if (prev_ptr == NULL) cbs = current_ptr->next; + else prev_ptr->next = current_ptr->next; + free(current_ptr); + return 0; + } + prev_ptr = current_ptr; + current_ptr = current_ptr->next; + } + } + if (error != NULL) { + /* No callback found, Display default error message. */ + fprintf(stderr, "Error: Request %lld: %s (%d)\n", + request_id, + error->message_, + error->code_); + } + return 0; +} + +static void tdcb_free() { + struct td_callback *current_ptr = cbs; + while (current_ptr != NULL) { + struct td_callback *bak = current_ptr; + current_ptr = current_ptr->next; + free(bak); + } + cbs = NULL; +} + +void td_init() { + sighandler_init(); + td = TdCClientCreateId(); + TdDestroyObjectObject(TdCClientExecute((struct TdFunction *) TdCreateObjectSetLogVerbosityLevel(0))); + td_send(TdCreateObjectGetOption("version"), &fetal_cb, NULL); +} + +void td_free() { + sighandler_close(); + tdcb_free(); +} + +void tg_close() { + if (td == -1) return; + td_send(TdCreateObjectClose(), NULL, NULL); +} + +void fetal_cb(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + if (!successful) { + tg_close(); + } +} + +static void auth(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg) { + if (closing || successful) return; + if (error != NULL) { /* error == NULL when caused from update handler */ + fprintf(stderr, "Invalid bot token or API ID / Hash: %s (%d)\n", + error->message_, + error->code_); + tg_close(); + return; + } + td_send(TdCreateObjectCheckAuthenticationBotToken((char *) cmd.bot_token), + &auth, + NULL); +} + +static int handle_auth(const struct TdUpdateAuthorizationState *update) { + switch (update->authorization_state_->ID) { + case CODE_AuthorizationStateWaitTdlibParameters: + td_send(TdCreateObjectSetTdlibParameters(TdCreateObjectTdlibParameters( + cmd.test_dc, + (char *) cmd.td_path, + NULL, + false, + false, + false, + false, + cmd.api_id, + (char *) cmd.api_hash, + "en", + "Desktop", + "0.0", + "Channel Helper Bot 1.0", + false, + true + )), + &fetal_cb, + NULL); + return 0; + case CODE_AuthorizationStateWaitPhoneNumber: { + if (cmd.logout) { + tg_close(); + return 0; + } + auth(false, NULL, NULL, NULL); + return 0; + } + case CODE_AuthorizationStateReady: { + if (cmd.logout) { + td_send(TdCreateObjectLogOut(), &fetal_cb, NULL); + return 0; + } + return post_auth(); + } + case CODE_AuthorizationStateWaitEncryptionKey: { + td_send(TdCreateObjectCheckDatabaseEncryptionKey(TdCreateObjectBytes((unsigned char *) {0x0}, 0)), + &fetal_cb, NULL); + return 0; + } + case CODE_AuthorizationStateLoggingOut: { + return 0; + } + /* Closed state is handled in the main loop. */ + case CODE_AuthorizationStateClosing: { + closing = true; + return 0; + } + case CODE_AuthorizationStateWaitOtherDeviceConfirmation: { + struct TdAuthorizationStateWaitOtherDeviceConfirmation *waitOtherDeviceConfirmation = + (struct TdAuthorizationStateWaitOtherDeviceConfirmation *)update->authorization_state_; + printf("Please scan the QR code of the following link using another Telegram seession:\n%s\n", + waitOtherDeviceConfirmation->link_); + return 0; + } + default: { + fprintf(stderr, "Unsupported authorization state: %d. Aborted.\n", + update->authorization_state_->ID); + tg_close(); + return 0; + } + } +} + +static int handle_option(const struct TdUpdateOption *update) { + if (!strcmp("my_id", update->name_) && + update->value_->ID == CODE_OptionValueInteger) { + const struct TdOptionValueInteger *integer = + (struct TdOptionValueInteger *) update->value_; + my_id = integer->value_; + } + return 0; +} + +static int handle_update(const struct TdUpdate *update) { + switch (update->ID) { + case CODE_UpdateAuthorizationState: + return handle_auth((struct TdUpdateAuthorizationState *) update); + case CODE_UpdateNewMessage: + return handle_message((struct TdUpdateNewMessage *) update); + case CODE_UpdateNewInlineQuery: + return handle_inline((struct TdUpdateNewInlineQuery *) update); + case CODE_UpdateOption: + return handle_option((struct TdUpdateOption *) update); + default: + return 0; + } +} + +void td_loop() { + struct TdResponse response; + while (1) { + response = TdCClientReceive(5); + struct TdObject *obj = response.object; + if (obj == NULL) continue; + const bool is_update = response.request_id == 0; + if (is_update && + obj->ID == CODE_UpdateAuthorizationState && + ((struct TdUpdate *) obj)->ID == CODE_UpdateAuthorizationState && + ((struct TdUpdateAuthorizationState *) obj)->authorization_state_->ID == + CODE_AuthorizationStateClosed) { + closing = true; + TdDestroyObjectObject(obj); + return; + } + if (is_update) { + handle_update((struct TdUpdate *) obj); + TdDestroyObjectObject(obj); + continue; + } + switch (obj->ID) { + case CODE_Ok: + tdcb_call(response.request_id, true, NULL, NULL); + break; + case CODE_Error: { + struct TdError *error = + (struct TdError *) obj; + tdcb_call(response.request_id, false, NULL, error); + break; + } + default: + tdcb_call(response.request_id, true, obj, NULL); + break; + } + TdDestroyObjectObject(obj); + } +} diff --git a/tdutils.h b/tdutils.h new file mode 100644 index 0000000..c0653d5 --- /dev/null +++ b/tdutils.h @@ -0,0 +1,28 @@ +#ifndef _TDUTILS_H +#define _TDUTILS_H + +#include +#include + +extern long long my_id; +extern bool closing; + +void td_init(); + +void td_free(); + +void td_loop(); + +void tg_close(); + +int td_send(void *func, void (*cb)(bool, struct TdObject *, struct TdError *, void *), void *cb_arg); + +void fetal_cb(bool successful, struct TdObject *result, struct TdError *error, void *cb_arg); + +int post_auth(); + +int handle_message(struct TdUpdateNewMessage *update); + +int handle_inline(struct TdUpdateNewInlineQuery * update); + +#endif /* _TDUTILS_H */ \ No newline at end of file -- cgit v1.2.3