diff options
author | Trumeet <yuuta@yuuta.moe> | 2022-04-01 21:13:31 -0700 |
---|---|---|
committer | Trumeet <yuuta@yuuta.moe> | 2022-04-01 21:13:31 -0700 |
commit | 318a1ef88bb5ea09ff4cf953908aef5c76735a46 (patch) | |
tree | 9cdd8be7679e6a336af7a82ca4947b3ffdac97b2 | |
download | ksyxbot-318a1ef88bb5ea09ff4cf953908aef5c76735a46.tar ksyxbot-318a1ef88bb5ea09ff4cf953908aef5c76735a46.tar.gz ksyxbot-318a1ef88bb5ea09ff4cf953908aef5c76735a46.tar.bz2 ksyxbot-318a1ef88bb5ea09ff4cf953908aef5c76735a46.zip |
First Commit
-rw-r--r-- | .gitignore | 76 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | .idea/.gitignore | 8 | ||||
-rw-r--r-- | .idea/inspectionProfiles/Project_Default.xml | 6 | ||||
-rw-r--r-- | .idea/ksyxbot.iml | 2 | ||||
-rw-r--r-- | .idea/misc.xml | 4 | ||||
-rw-r--r-- | .idea/modules.xml | 8 | ||||
-rw-r--r-- | .idea/vcs.xml | 7 | ||||
-rw-r--r-- | CMakeLists.txt | 20 | ||||
-rw-r--r-- | arch/.gitignore | 5 | ||||
-rw-r--r-- | arch/PKGBUILD | 73 | ||||
-rw-r--r-- | arch/botd.conf | 3 | ||||
-rw-r--r-- | arch/ksyxbotd.service | 13 | ||||
-rw-r--r-- | arch/sysusers.conf | 1 | ||||
-rw-r--r-- | arch/tmpfiles.conf | 2 | ||||
-rw-r--r-- | botd.h | 29 | ||||
-rw-r--r-- | cmdline.c | 91 | ||||
-rw-r--r-- | db.c | 124 | ||||
-rw-r--r-- | db.h | 22 | ||||
-rw-r--r-- | log.c | 44 | ||||
-rw-r--r-- | log.h | 50 | ||||
-rw-r--r-- | logic.h | 15 | ||||
-rw-r--r-- | main.c | 50 | ||||
-rw-r--r-- | query.c | 155 | ||||
-rw-r--r-- | refresh.c | 121 | ||||
-rw-r--r-- | sql/0_1.sql | 10 | ||||
-rw-r--r-- | store.c | 125 | ||||
m--------- | td | 0 | ||||
-rw-r--r-- | tdutils.c | 368 | ||||
-rw-r--r-- | tdutils.h | 28 |
30 files changed, 1463 insertions, 0 deletions
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 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> + </profile> +</component>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module classpath="CMake" type="CPP_MODULE" version="4" />
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" /> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/ksyxbot.iml" filepath="$PROJECT_DIR$/.idea/ksyxbot.iml" /> + </modules> + </component> +</project>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/td" vcs="Git" /> + </component> +</project>
\ 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 <yuuta@yuuta.moe> +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 - @@ -0,0 +1,29 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#ifndef KSYXBOT_BOTD_H +#define KSYXBOT_BOTD_H + +#include <stdbool.h> + +#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 <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <limits.h> +#include <inttypes.h> +#include <errno.h> + +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 @@ -0,0 +1,124 @@ +// +// Created by yuuta on 11/28/21. +// + +#include "db.h" +#include "log.h" +#include "botd.h" + +#include <stdio.h> +#include <sqlite3.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> + +#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; +} @@ -0,0 +1,22 @@ +// +// Created by yuuta on 11/28/21. +// + +#ifndef _DB_H +#define _DB_H + +#include <sqlite3.h> +#include <stdio.h> +#include <math.h> +#include <stdlib.h> + +#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 */ @@ -0,0 +1,44 @@ +/* + * Created by yuuta on 1/1/22. + */ + +#include "log.h" + +#include <stdio.h> +#include <stdarg.h> +#include <assert.h> + +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"); +} @@ -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 */ @@ -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 */ @@ -0,0 +1,50 @@ +#include "db.h" +#include "log.h" +#include "tdutils.h" +#include "botd.h" +#include "logic.h" + +#include <stdlib.h> +#include <string.h> + +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 @@ -0,0 +1,155 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "tdutils.h" +#include "logic.h" +#include "log.h" +#include "db.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <assert.h> + +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 <stdio.h> +#include <stdbool.h> + +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 @@ -0,0 +1,125 @@ +/* + * Created by yuuta on 4/1/22. + */ + +#include "log.h" +#include "logic.h" +#include "db.h" +#include "botd.h" + +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +/* 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 +Subproject 461b740987101972cce65d1d5c996f455e4891e 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 <stdio.h> +#include <td/telegram/td_c_client.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <limits.h> +#include <pthread.h> +#include <signal.h> +#include <stdatomic.h> +#include <stdbool.h> + +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 <td/telegram/td_c_client.h> +#include <stdbool.h> + +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 |