From d5592d5ee39017b9b7ad775999b07d0899a9d099 Mon Sep 17 00:00:00 2001 From: mrbesen Date: Mon, 20 Nov 2023 19:57:37 +0100 Subject: [PATCH] initial --- .gitignore | 20 + .gitmodules | 6 + .vscode/c_cpp_properties.json | 20 + .vscode/launch.json | 50 +++ .vscode/tasks.json | 31 ++ Makefile | 85 ++++ inc/chat.h | 67 +++ inc/config.h | 18 + inc/message.h | 55 +++ inc/msgident.h | 7 + inc/replymarkup.h | 91 ++++ inc/slimchat.h | 9 + inc/tgclient.h | 180 ++++++++ inc/tgtui.h | 36 ++ inc/typeconverter.h | 26 ++ inc/user.h | 36 ++ inc/userstatus.h | 43 ++ inc/view.h | 19 + inc/viewchatlist.h | 28 ++ src/config.cpp | 11 + src/main.cpp | 35 ++ src/replymarkup.cpp | 55 +++ src/tgclient.cpp | 761 ++++++++++++++++++++++++++++++++++ src/tgtui.cpp | 110 +++++ src/typeconverter.cpp | 270 ++++++++++++ src/userstatus.cpp | 49 +++ src/view.cpp | 13 + src/viewchatlist.cpp | 61 +++ tests/main.cpp | 70 ++++ tests/sampletest.cpp | 12 + tests/test.h | 43 ++ thirdparty/Log | 1 + thirdparty/td | 1 + 33 files changed, 2319 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Makefile create mode 100644 inc/chat.h create mode 100644 inc/config.h create mode 100644 inc/message.h create mode 100644 inc/msgident.h create mode 100644 inc/replymarkup.h create mode 100644 inc/slimchat.h create mode 100644 inc/tgclient.h create mode 100644 inc/tgtui.h create mode 100644 inc/typeconverter.h create mode 100644 inc/user.h create mode 100644 inc/userstatus.h create mode 100644 inc/view.h create mode 100644 inc/viewchatlist.h create mode 100644 src/config.cpp create mode 100644 src/main.cpp create mode 100644 src/replymarkup.cpp create mode 100644 src/tgclient.cpp create mode 100644 src/tgtui.cpp create mode 100644 src/typeconverter.cpp create mode 100644 src/userstatus.cpp create mode 100644 src/view.cpp create mode 100644 src/viewchatlist.cpp create mode 100644 tests/main.cpp create mode 100644 tests/sampletest.cpp create mode 100644 tests/test.h create mode 160000 thirdparty/Log create mode 160000 thirdparty/td diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e44299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +*.bin +*.out +build/ +*.exe +.gdb_history + +*.so +*.bmp +*.d +*.db + +test +.vscode/settings.json + +tgtui +tgtui_strip +log.txt +tdlib/ + +thirdparty/td/tdlib diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f07350a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "thirdparty/td"] + path = thirdparty/td + url = https://github.com/tdlib/td +[submodule "thirdparty/Log"] + path = thirdparty/Log + url = https://git.mrbesen.de/MrBesen/Log.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..8cec27f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,20 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/src/", + "${workspaceFolder}/thirdparty/Log/", + "${workspaceFolder}/src/**", + "${workspaceFolder}/inc/**", + "${workspaceFolder}/thirdparty/td/tdlib/include/**" + ], + "defines": [], + "compilerPath": "/usr/bin/g++", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b0e5bd5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "test Debuggen (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/test", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und Einrückung für \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "make test", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debuggen (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/tgtui", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und Einrückung für \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "make all", + "miDebuggerPath": "/usr/bin/gdb" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..e68935d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "make all", + "type": "shell", + "command": "make -j all", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "make clean", + "type": "shell", + "command": "make clean", + "problemMatcher": [] + }, + { + "label": "make test", + "type": "shell", + "command": "make -j test", + "problemMatcher": ["$gcc"], + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..392ad2e --- /dev/null +++ b/Makefile @@ -0,0 +1,85 @@ +# Author Yannis Gerlach +# Hochschule Osnabrück +# 13.11.2020 + +# `make clean all` nicht mit -j verwenden! -> race condition im make file +# statdessen: `make clean; make all -j` verwenden + +NAME = tgtui +NAMETEST = test +CFLAGS = -std=c++17 -O2 -g -pipe -Wall -Wextra -Wno-unused-parameter -Wpedantic -rdynamic #-march=native -Wall +CXX = g++ +SRCF = src/ +BUILDDIR = build/ +TESTF = tests/ +DEPF = $(BUILDDIR)deps/ +INCF = ./inc/ +INCFS = $(shell find $(INCF) -type d) + +LOGF = ./thirdparty/Log/ +LOGO = $(LOGF)Log.o + +TDLIBF = ./thirdparty/td/tdlib/ +TDLIBAF = $(TDLIBF)lib/ +# TDLIBA = $(shell find $(TDLIBF)lib/ -type f -name "*.a") +TDLIBA = $(TDLIBF)lib/libtdclient.a $(TDLIBF)lib/libtdcore.a $(TDLIBF)lib/libtdapi.a $(TDLIBF)lib/libtddb.a $(TDLIBF)lib/libtdactor.a $(TDLIBF)lib/libtdsqlite.a $(TDLIBF)lib/libtdnet.a $(TDLIBF)lib/libtdutils.a + +INCLUDES = -I$(LOGF) $(addprefix -I, $(INCFS)) -I$(TDLIBF)include +LDFLAGS = -pthread -lz -lssl -lcrypto -lncurses + +SRCFILES = $(shell find $(SRCF) -name "*.cpp") +OBJFILES = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(patsubst %.cpp, %.o, $(SRCFILES))) $(LOGO) +DEPFILES = $(wildcard $(DEPF)*.d) + +SOURCEDIRS = $(shell find $(SRCF) -type d -printf "%p/\n") +BUILDDIRS = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(SOURCEDIRS)) + +OBJFILESTEST = $(filter-out $(BUILDDIR)main.o, $(OBJFILES)) + +INCLUDES += $(addprefix -I, $(SOURCEDIRS)) + +all: $(NAME) runtest + +$(NAME): $(BUILDDIRS) $(DEPF) $(OBJFILES) $(TDLIBA) + @echo "Linking $@" + @$(CXX) $(CFLAGS) -o $@ $(filter %.o, $^) $(filter %.a, $^) $(LDFLAGS) + +$(BUILDDIR)%.o: $(SRCF)%.cpp + @echo "Compiling: $@" + @$(CXX) $(CFLAGS) $(INCLUDES) $< -MM -MT $@ > $(DEPF)$(subst /,_,$*).d + @$(CXX) -c -o $@ $(CFLAGS) $(INCLUDES) $< + +$(NAME)_strip: $(NAME) + @echo "Strip $<" + @strip -o $@ $< + +%/: + mkdir -p $@ + +clean-depends: + $(RM) -r $(DEPF) + +$(LOGO): + $(MAKE) -C $(LOGF) all + +clean: + $(RM) -r $(NAME) $(BUILDDIR) $(NAMETEST) $(NAME)_strip + $(MAKE) -C $(LOGF) $@ + +$(NAMETEST): $(BUILDDIRS) $(DEPF) $(TESTF)*.cpp $(OBJFILESTEST) $(TDLIBA) + @echo "Compiling tests" + @$(CXX) -o $@ $(filter %.o, $^) $(filter %.cpp, $^) $(filter %.a, $^) $(CFLAGS) -I$(SRCF) $(INCLUDES) $(LDFLAGS) + +runtest: $(NAMETEST) + @echo "Running tests" + ./$< + +.PHONY: clean all $(NAMETEST) clean-depends runtest + +remaketdlib: + $(RM) -r ./thirdparty/td/build/ ./thirdparty/td/tdlib/ + mkdir -p ./thirdparty/td/build/ + cd ./thirdparty/td/build/ ; cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. + $(MAKE) -C ./thirdparty/td/build/ install + +include $(DEPFILES) diff --git a/inc/chat.h b/inc/chat.h new file mode 100644 index 0000000..c1c8890 --- /dev/null +++ b/inc/chat.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +enum class ChatType : uint32_t { + UNKNOWN = 0, + GROUP = 1, + PRIVATE, + SECRET, + SUPERGROUP, + CHANNEL +}; + +struct SupergroupInfo { + std::string username; + int64_t created; + bool signMessages; + bool verified; + std::string restriction; + bool scam; + bool fake; +}; + +struct Chat { + //basic information + int64_t id; + int64_t supergroupid; //diffrent than id + ChatType type; + std::string title; + bool hasPhoto; + + //full information + std::string description; + uint32_t memberCount; + uint32_t adminCount; + uint32_t restrictedCount; + uint32_t bannedCount; + int64_t linkedGroupId; + uint32_t slowmode; + uint64_t slowModeExpiresTS; //timestamp, when slow mode ends + int64_t sitckerSetId; // 0 = none + + std::string link = ""; //invite link, may be empty + + //loacation + bool hasLocation = false; + std::string address = ""; + double latitude = 0; + double longitude = 0; + double locationAccourcay = 0; + + //other supergroupInforation + SupergroupInfo* moreinfo = nullptr; +}; + +struct ChatIdentifier { + int64_t chatid = 0; + std::string username = ""; + + bool operator<(const ChatIdentifier& rhs) const; + bool operator<(const int64_t rhs) const; +}; + +bool constexpr operator<(const int64_t l, const ChatIdentifier& rhs) { + return l < rhs.chatid; +} \ No newline at end of file diff --git a/inc/config.h b/inc/config.h new file mode 100644 index 0000000..0b1702b --- /dev/null +++ b/inc/config.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +struct Config { + + Config(); + + int apiid; + std::string apihash; + std::string phoneNumber; + std::string tgCloudPassword; + + static Config config; +}; + + + diff --git a/inc/message.h b/inc/message.h new file mode 100644 index 0000000..9b19c8f --- /dev/null +++ b/inc/message.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +#include "replymarkup.h" + +enum class MessageType : uint32_t { + UNKNOWN = 0, + UNSUPPORTED = 1, + TEXT = 2, + + ANIMATION, + AUDIO, + CONTACT, + DOCUMENT, + LOCATION, + PHOTO, + POLL, + STICKER, + VIDEO, + VENUE, + VOICE, + + SERVICE +}; + +const std::string MessageTypeTypes[] {"unknown", "unsupported", "text", "animation", "audio", "contact", "document", "location", "photo", "poll", "sticker", "video", "venue", "voice", "service"}; + +const std::string& convertMessageType(MessageType); +MessageType convertMessageType(const std::string& in); + +struct Message { + int64_t id; + int64_t chat; + int64_t sender; + MessageType type; + std::string text; + bool isDeleted; + bool isOutgoing; + bool isPinned; + bool isEditable; + bool isForwardable; + uint64_t sendDate; + uint64_t editDate; + int64_t messageThreadId; + int64_t viaBotid; + int64_t mediaAlbumId; + + std::shared_ptr replyMarkup; + + bool operator==(const Message&) const; + bool operator!=(const Message&) const; +}; \ No newline at end of file diff --git a/inc/msgident.h b/inc/msgident.h new file mode 100644 index 0000000..982747e --- /dev/null +++ b/inc/msgident.h @@ -0,0 +1,7 @@ +#pragma once +#include + +struct MsgIdent { + int64_t chat; + int64_t msgid; +}; \ No newline at end of file diff --git a/inc/replymarkup.h b/inc/replymarkup.h new file mode 100644 index 0000000..c693b61 --- /dev/null +++ b/inc/replymarkup.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +// interface +class ReplyMarkup { +public: + ReplyMarkup() {} + virtual ~ReplyMarkup() {} +}; + + +class InlineButton { +public: + virtual ~InlineButton() {} + + enum class ButtonType : uint32_t { + CALLBACK = 0, + CALLBACKPASSWORD, + LOGINURL, + SWITCHINLINE, + URL + }; + + static const std::string ButtonTypeTypes[]; + static const std::string& getButtonTypeString(ButtonType t); + + std::string text; + + virtual ButtonType getButtonType() const = 0; + virtual void write(std::ostream& os) const = 0; +}; + +class InlineCallbackButton : public InlineButton { +public: + + std::string data; + + virtual ButtonType getButtonType() const override; + virtual void write(std::ostream& os) const override; +}; + +class InlineCallbackPasswordButton : public InlineButton { +public: + + std::string data; + + virtual ButtonType getButtonType() const override; + virtual void write(std::ostream& os) const override; +}; + +class InlineLoginURLButton : public InlineButton { +public: + + std::string url; + int32_t id; + std::string forwardText; + + virtual ButtonType getButtonType() const override; + virtual void write(std::ostream& os) const override; +}; + +class InlineSwitchInlineButton : public InlineButton { +public: + + std::string query; + bool inCurrentChat; + + virtual ButtonType getButtonType() const override; + virtual void write(std::ostream& os) const override; +}; + +class InlineURLButton : public InlineButton { +public: + + std::string url; + + virtual ButtonType getButtonType() const override; + virtual void write(std::ostream& os) const override; +}; + +class InlineReplyMarkup : public ReplyMarkup { +public: + virtual ~InlineReplyMarkup(); + + std::vector buttons; +}; + diff --git a/inc/slimchat.h b/inc/slimchat.h new file mode 100644 index 0000000..5312d1f --- /dev/null +++ b/inc/slimchat.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +struct SlimChat { + int64_t chatId; + std::string name; +}; diff --git a/inc/tgclient.h b/inc/tgclient.h new file mode 100644 index 0000000..91f58c1 --- /dev/null +++ b/inc/tgclient.h @@ -0,0 +1,180 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "userstatus.h" +#include "user.h" + +namespace td_api = td::td_api; +using Object = td_api::object_ptr; + +template +using objptr = td_api::object_ptr; +using MessageCallback = std::function)>; +using StatusCallback = std::function; +using DraftCallback = std::function; +using ActionCallback = std::function; +using DeleteCallback = std::function; +using UserUpdateCallback = std::function; +using EditMessageCallback = std::function)>; +using IndexDoneCallback = std::function; +using FileUpdateCallback = std::function)>; +using NewChatCallback = std::function)>; +using ChatFiltersCallback = std::function)>; + +class TGClient { +public: + static const uint64_t STATICHANDLERCOUNT = 128; + static void(TGClient::* STATICHANDLERS [STATICHANDLERCOUNT])(objptr); + + TGClient(std::function initDoneCallback = {}); + + void setAuthData(std::function getAuthCodeCallback); + void loop(); + void stop(); + + void registerNewMessageHandler(MessageCallback mclb); + void registerStatusUpdateHandler(StatusCallback sclb); + void registerDraftHandler(DraftCallback dclb); + void registerActionHandler(ActionCallback clb); + void registerDeleteHandler(DeleteCallback dclb); + void registerUserUpdateHandler(UserUpdateCallback uuclb); + void registerEditMessageHandler(EditMessageCallback emclb); + void registerIndexDoneHandle(IndexDoneCallback idclb); + void registerFileUpdateCallback(FileUpdateCallback fuclb); + void registerNewChatCallback(NewChatCallback ncclb); + void registerChatFiltersCallback(ChatFiltersCallback cfclb); + + //exposed apicalls + void deleteMessage(int64_t chatid, int64_t messageid, bool forall = true); + + void screenShottaken(int64_t chatid); + + void reply(const objptr& rplyto, const std::string& awnser); + void reply(int64_t chat, int64_t msg, const std::string& awnser); + + void editMessage(const objptr& toEdit, const std::string& newText); + void editMessage(const objptr& toEdit, const std::string& newText, std::vector>& entities); + + void editMessageCaption(const objptr& toEdit, const std::string& newText, std::vector>& entities); + + void sendCallbackQuery(int64_t chat, int64_t messageid, const std::string& payload = ""); + + // mute for is in seconds (more than 604800 => forever) + void muteChat(int64_t chatid, int32_t mutefor = 1<<30); + + void addChatToChatListFilter(int64_t chatid, int32_t chatfilterid); + + //index chat (editMessageCallback is called for every Message in the chat) + void indexChat(int64_t chatid); + void getAllTextMessages(int64_t chatid, std::function)>); + + void downloadFile(int32_t file_id); + + void sendTextMessageToSelf(const std::string& text); + + void getMessage(int64_t chatid, int64_t messageid, std::function)> f); + + //drafts + void setDraft(int64_t chat, const std::string& text = "", int64_t replyto = 0, int64_t messageThread = 0); + void clearDraft(int64_t chat, int64_t messageThread = 0); + bool hasDraft(int64_t chat) const; + + const User* getCachedUser(int64_t userid); + + int64_t me = 0; //my chatid + +private: + bool shouldrun = false; + bool initDone = false; + + std::unique_ptr client_manager_; + std::int32_t client_id_{0}; + + td_api::object_ptr authorization_state_; + bool are_authorized_{false}; + bool need_restart_{false}; + std::uint64_t current_query_id_ = STATICHANDLERCOUNT; + std::uint64_t authentication_query_id_{0}; + + std::map> handlers_; //handler list maps updateid -> callback + std::map> drafts; // chatid -> draft message / list of user drafts (does not contain drafts from the client itself) + + //user cache + struct CachedUser : public User { + uint64_t lastaccessed = 0; + }; + std::map usercache; //list of cached users + std::multimap usercache_timeout; //list of last access -> to know when to clean up which object + static const uint32_t MAXCACHEDUSERCOUNT; //at how many users are allowed until the cache si cleand + static const uint64_t CACHEDUSERCOUNTCLEANUPTIME; //how old is a cached user allowed to get before beeing purged from the cache (in seconds) + + std::function initDoneCallback; + + //callbacks + std::function getAuthCodeCallback; + MessageCallback messageCallback; + StatusCallback statusCallback; + DraftCallback draftCallback; + ActionCallback actionCallback; + DeleteCallback deleteCallback; + UserUpdateCallback userupdCallback; + EditMessageCallback editMessageCallback; + IndexDoneCallback indexDoneCallback; + FileUpdateCallback fileUpdateCallback; + NewChatCallback newChatCallback; + ChatFiltersCallback chatFiltersCallback; + + //user cache + void accessUser(std::map::iterator it); //iterator to cached user object in usercache + void addUser(CachedUser& u); + void removeUser(std::map::iterator it); //iterator to cached user object in usercache) + void checkUserCache(); + + //wrapped requests + void requestMessages(int64_t chatid, int64_t from_message_id = 0, std::function onDone = {}, std::function)> forMessage = {}); + + //helper + void iterate(const td_api::array>& messages, std::function handler); + void setOption(const std::string& name, objptr val); + void setOptions(); + + //load me and chat list, initDoneCallback is triggered after that + void loadInit(); + void loadInitInternalCallback(Object o); + + void restart(); + void send_query(td_api::object_ptr f, std::function handler = {}); + + void send_staticquery(td_api::object_ptr f, uint64_t handlerid); + + template + void send_wrappedquery(td_api::object_ptr f, std::function)> handler = {}, std::function onError = {}, bool printError = true); + + template + static bool catchErrors(const Object& o, std::function onError = {}, bool printError = true); + + template + void send_inplace(Args... args, std::function)> handler = {}, bool printError = true); + + void process_response(td::ClientManager::Response response); + + void process_update(td_api::object_ptr update); + + auto create_authentication_query_handler(); + + void on_authorization_state_update(); + + void check_authentication_error(Object object); + + std::uint64_t next_query_id(); +}; \ No newline at end of file diff --git a/inc/tgtui.h b/inc/tgtui.h new file mode 100644 index 0000000..e1336e2 --- /dev/null +++ b/inc/tgtui.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "slimchat.h" +#include "tgclient.h" + +class View; + +class TgTUI { +public: + + TgTUI(); + ~TgTUI(); + + void run(); + void stop(); + + const std::vector& getChats(); + +private: + void initDoneCB(); + + void threadLoop(); + + void handleNewChat(objptr chat); + + TGClient tgclient; + + View* currentView = nullptr; + std::vector views; + + std::vector chats; + bool shouldRun = false; + std::thread tuiThread; +}; diff --git a/inc/typeconverter.h b/inc/typeconverter.h new file mode 100644 index 0000000..f7cbd51 --- /dev/null +++ b/inc/typeconverter.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "chat.h" +#include "message.h" +#include "user.h" + +namespace td_api = td::td_api; +using Object = td_api::object_ptr; + +//converts between tdlib types and own types + +ChatType convertChatType(const td_api::ChatType& in); + +void convertUser(const td_api::user& in, User& out); +UserType convertUserType(const td_api::UserType& in); + +void convertMessage(const td_api::message& in, Message& out); +MessageType convertMessageTypeandText(const td_api::MessageContent& cont, std::string& textout); + +ReplyMarkup* convertReplyMarkup(const td_api::ReplyMarkup* in); +InlineButton* convertInlineButton(const td_api::inlineKeyboardButton* in); + +const std::string& getMessageText(const td_api::object_ptr& msg); +td_api::formattedText* getMessageFormattedText(td_api::message& msg, bool& isText); \ No newline at end of file diff --git a/inc/user.h b/inc/user.h new file mode 100644 index 0000000..206f565 --- /dev/null +++ b/inc/user.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +enum UserType { + BOT, + DELETED, + REGULAR, + UNKNOWN, +}; + +const std::string UserTypeTypes[] {"bot", "deleted", "regular", "unknown"}; + +const std::string& convertUserType(UserType); +UserType convertUserType(const std::string& in); + +// complete user infomation +struct User { + int64_t tgid; + std::string firstname; + std::string lastname; + std::string username; + std::string phonenumber; + bool isContact; + bool isMutalContact; + bool isVerified; + bool isSupport; + std::string restriction; + bool isScam; + bool haveAccess; + UserType type; + + bool operator==(const User&) const; + bool operator!=(const User&) const; +}; \ No newline at end of file diff --git a/inc/userstatus.h b/inc/userstatus.h new file mode 100644 index 0000000..d3b039d --- /dev/null +++ b/inc/userstatus.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +namespace UserStatus { + +enum UserStatus : uint8_t { + EMPTY = 0, + ONLINE, + OFFLINE, + RECENTLY, + LASTWEEK, + LASTMONTH +}; + +UserStatus getStatus(int32_t status); +const std::string& statustoString(UserStatus); + +} + +namespace ChatAction { + +enum ChatAction : uint8_t { + CANCEL = 0, + CHOOSINGCONTACT, + CHOOSINGLOCATION, + RECORDINGVIDEO, + RECORDINGVIDEONOTE, + RECORDINGVOICENOTE, + PLAYGAME, + TYPING, + UPLOADINGDOCUMENT, + UPLOADINGPHOTO, + UPLODAINGVIDEO, + UPLOADINGVIDEONOTE, + UPLOADINGVOICENOTE +}; + +ChatAction getAction(int32_t status); +const std::string& actiontoString(ChatAction); + +} \ No newline at end of file diff --git a/inc/view.h b/inc/view.h new file mode 100644 index 0000000..5ca9b23 --- /dev/null +++ b/inc/view.h @@ -0,0 +1,19 @@ +#pragma once + +class TgTUI; + +class View { +public: + View(TgTUI& tgtui); + virtual ~View(); + + virtual void open(); + virtual void close(); + + virtual void paint() = 0; + + virtual bool keyIn(int key); + +protected: + TgTUI& tgtui; +}; diff --git a/inc/viewchatlist.h b/inc/viewchatlist.h new file mode 100644 index 0000000..5f284e8 --- /dev/null +++ b/inc/viewchatlist.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "slimchat.h" +#include "view.h" + +class ViewChatList : public View { +public: + ViewChatList(TgTUI& tgtui); + + virtual void open() override; + virtual void close() override; + + virtual void paint() override; + + virtual bool keyIn(int key) override; + + int64_t getSelectedChatId(); + +private: + std::vector chats; + int32_t currentChatOffset = 0; + int32_t selectedChatRow = 0; + + int maxRows, maxCols; +}; \ No newline at end of file diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..0002442 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,11 @@ +#include "config.h" + +Config Config::config; + +Config::Config() : + apiid(94575), // taken from td/example/cpp/td_example.cpp + apihash("a3406de8d171bb422bb6ddf3bbd800e2"), + phoneNumber(""), + tgCloudPassword("") +{ +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..36c64cb --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include + +#include "tgtui.h" + +static TgTUI* tui = nullptr; + +void sig_handler(int sig_num) { + Log::info << "signalHandler triggered"; + if(tui) + tui->stop(); + (void) sig_num; +} + +int main(int argc, const char** argv) { + Log::Deleter log; + Log::setConsoleLogLevel(Log::Level::off); + Log::addLogfile("log.txt", Log::Level::trace, true); + + Log::info << "Hello, World!"; + + TgTUI tgtui; + tui = &tgtui; + + //register signal handler + signal(SIGINT, sig_handler); + + tgtui.run(); + + tui = nullptr; + + return 0; +} diff --git a/src/replymarkup.cpp b/src/replymarkup.cpp new file mode 100644 index 0000000..394721d --- /dev/null +++ b/src/replymarkup.cpp @@ -0,0 +1,55 @@ +#include "replymarkup.h" + +const std::string InlineButton::ButtonTypeTypes[] = {"Callback", "CallbackPassword", "LoginURL", "SwitchInline", "URL"}; + +const std::string& InlineButton::getButtonTypeString(ButtonType t) { + if(t <= ButtonType::URL) + return ButtonTypeTypes[(uint32_t) t]; + return ButtonTypeTypes[0]; +} + +InlineButton::ButtonType InlineCallbackButton::getButtonType() const { + return ButtonType::CALLBACK; +} + +void InlineCallbackButton::write(std::ostream& os) const { + os << data; +} + +InlineButton::ButtonType InlineCallbackPasswordButton::getButtonType() const { + return ButtonType::CALLBACKPASSWORD; +} + +void InlineCallbackPasswordButton::write(std::ostream& os) const { + os << data; +} + +InlineButton::ButtonType InlineLoginURLButton::getButtonType() const { + return ButtonType::LOGINURL; +} + +void InlineLoginURLButton::write(std::ostream& os) const { + os << "url: " << url << " id: " << id << " fwdText: " << forwardText; +} + +InlineButton::ButtonType InlineSwitchInlineButton::getButtonType() const { + return ButtonType::SWITCHINLINE; +} + +void InlineSwitchInlineButton::write(std::ostream& os) const { + os << "query: " << query << " inCurrentChat: " << inCurrentChat; +} + +InlineButton::ButtonType InlineURLButton::getButtonType() const { + return ButtonType::URL; +} + +void InlineURLButton::write(std::ostream& os) const { + os << url; +} + +InlineReplyMarkup::~InlineReplyMarkup() { + for(InlineButton* btn : buttons) { + delete btn; + } +} \ No newline at end of file diff --git a/src/tgclient.cpp b/src/tgclient.cpp new file mode 100644 index 0000000..fb98b10 --- /dev/null +++ b/src/tgclient.cpp @@ -0,0 +1,761 @@ +#include "tgclient.h" + +#include +#include +#include +#include +#include + +#include "config.h" +#include "typeconverter.h" + +namespace pl = std::placeholders; + +// overloaded +namespace detail { +template +struct overload; + +template +struct overload : public F { + explicit overload(F f) : F(f) { } +}; +template +struct overload + : public overload + , overload { + overload(F f, Fs... fs) : overload(f), overload(fs...) { } + using overload::operator(); + using overload::operator(); +}; +} // namespace detail + +template +auto overloaded(F... f) { + return detail::overload(f...); +} + +class Placeholder : public td_api::Object { +public: + static const std::int32_t ID = 0; +}; + +const uint32_t TGClient::MAXCACHEDUSERCOUNT = 1024; +const uint64_t TGClient::CACHEDUSERCOUNTCLEANUPTIME = 86400; + +void(TGClient::* TGClient::STATICHANDLERS [])(objptr) = { + nullptr, // ids with 0 are handled with the authorisation handler + nullptr, // zero handle + &TGClient::loadInitInternalCallback, +}; + +static const uint64_t HANDLER_NULL = 1; + +TGClient::TGClient(std::function initDoneCallback) : initDoneCallback(initDoneCallback) { + td::ClientManager::execute(td_api::make_object(1)); + client_manager_ = std::make_unique(); + client_id_ = client_manager_->create_client_id(); + send_query(td_api::make_object("version")); +} + +void TGClient::setAuthData(std::function getAuthCodeCallback_) { + getAuthCodeCallback = getAuthCodeCallback_; +} + +void TGClient::loop() { + shouldrun = true; + while (shouldrun) { + if (need_restart_) { + restart(); + } else if (!are_authorized_) { + Log::warn << "not authorized"; + while(!are_authorized_) { + process_response(client_manager_->receive(1)); + } + } else { + //process updates + while(true) { + auto response = client_manager_->receive(0.1); + if (response.object) + process_response(std::move(response)); + else + break; + } + } + } + + Log::note << "Client Loop terminated"; +} + +void TGClient::stop() { + shouldrun = false; + + send_query(td_api::make_object()); +} + +void TGClient::registerNewMessageHandler(MessageCallback mclb) { + messageCallback = mclb; +} + +void TGClient::registerStatusUpdateHandler(StatusCallback sclb) { + statusCallback = sclb; +} + +void TGClient::registerDraftHandler(DraftCallback dclb) { + draftCallback = dclb; +} + +void TGClient::registerActionHandler(ActionCallback clb) { + actionCallback = clb; +} + +void TGClient::registerDeleteHandler(DeleteCallback dclb) { + deleteCallback = dclb; +} + +void TGClient::registerUserUpdateHandler(UserUpdateCallback uuclb) { + userupdCallback = uuclb; +} + +void TGClient::registerEditMessageHandler(EditMessageCallback emclb) { + editMessageCallback = emclb; +} + +void TGClient::registerIndexDoneHandle(IndexDoneCallback idclb) { + indexDoneCallback = idclb; +} + +void TGClient::registerFileUpdateCallback(FileUpdateCallback fuclb) { + fileUpdateCallback = fuclb; +} + +void TGClient::registerNewChatCallback(NewChatCallback ncclb) { + newChatCallback = ncclb; +} + +void TGClient::registerChatFiltersCallback(ChatFiltersCallback cfclb) { + chatFiltersCallback = cfclb; +} + +static auto createFormattedText(const std::string& in, std::vector>& entities) { + auto formattedtext = td::make_tl_object(); + formattedtext->text_ = in; + formattedtext->entities_.reserve(entities.size()); + for(uint32_t i = 0; i < entities.size(); ++i) { + auto& ref = entities.at(i); + formattedtext->entities_.push_back(td::make_tl_object(ref->offset_, ref->length_, std::move(ref->type_))); + } + return formattedtext; +} + +static auto createMessageInputTextEntities(const std::string& in, std::vector>& entities) { + auto formattedText = createFormattedText(in, entities); + return td::make_tl_object(std::move(formattedText), false, false); +} + +static auto createMessageInputText(const std::string& in) { + std::vector> entities; + return createMessageInputTextEntities(in, entities); +} + +void TGClient::deleteMessage(int64_t chatid, int64_t messageid, bool forall) { + send_staticquery(td::make_tl_object(chatid, td_api::array(1, messageid), forall), HANDLER_NULL); +} + +void TGClient::screenShottaken(int64_t chatid) { + td_api::array messageIds; + send_staticquery(td::make_tl_object(chatid, std::move(messageIds), td::make_tl_object(), false), HANDLER_NULL); +} + +void TGClient::reply(const objptr& rplyto, const std::string& awnser) { + reply(rplyto->chat_id_, rplyto->id_, awnser); +} + +void TGClient::reply(int64_t chat, int64_t msg, const std::string& awnser) { + auto inputmsgCont = createMessageInputText(awnser); + auto sendMsg = td::make_tl_object(); + sendMsg->chat_id_ = chat; + sendMsg->reply_to_ = td_api::make_object(chat, msg); + sendMsg->input_message_content_ = std::move(inputmsgCont); + send_staticquery(std::move(sendMsg), HANDLER_NULL); +} + +void TGClient::editMessage(const objptr& toEdit, const std::string& newText) { + std::vector> entities; + editMessage(toEdit, newText, entities); +} + +void TGClient::editMessage(const objptr& toEdit, const std::string& newText, std::vector>& entities) { + auto editMsg = td::make_tl_object(); + editMsg->chat_id_ = toEdit->chat_id_; + editMsg->message_id_ = toEdit->id_; + editMsg->input_message_content_ = createMessageInputTextEntities(newText, entities); + send_staticquery(std::move(editMsg), HANDLER_NULL); +} + +void TGClient::editMessageCaption(const objptr& toEdit, const std::string& newText, std::vector>& entities ) { + auto editMsg = td::make_tl_object(); + editMsg->chat_id_ = toEdit->chat_id_; + editMsg->message_id_ = toEdit->id_; + editMsg->caption_ = createFormattedText(newText, entities); + send_staticquery(std::move(editMsg), HANDLER_NULL); +} + +void TGClient::sendCallbackQuery(int64_t chat, int64_t messageid, const std::string& payload) { + Log::info << "sendCallbackQuery: " << chat << " " << messageid << " " << payload; + auto payloadobj = td::make_tl_object(); + payloadobj->data_ = payload; + auto sendCallback = td::make_tl_object(); + sendCallback->chat_id_ = chat; + sendCallback->message_id_ = messageid; + sendCallback->payload_ = std::move(payloadobj); + send_wrappedquery(std::move(sendCallback), [](objptr clb) { + Log::info << "CallbackAwnser - text: " << clb->text_ << " showAlert: " << clb->show_alert_ << " url: " << clb->url_; + }); +} + +void TGClient::muteChat(int64_t chatid, int32_t mutefor) { + auto settings = td::make_tl_object(); + settings->use_default_mute_for_ = false; + settings->mute_for_ = mutefor; + settings->use_default_sound_ = true; + settings->use_default_show_preview_ = true; + settings->use_default_disable_pinned_message_notifications_ = true; + settings->use_default_disable_mention_notifications_ = true; + send_staticquery(td::make_tl_object(chatid, std::move(settings)), HANDLER_NULL); +} + +void TGClient::addChatToChatListFilter(int64_t chatid, int32_t chatfilterid) { + send_staticquery(td::make_tl_object(chatid, td::make_tl_object(chatfilterid)), HANDLER_NULL); +} + +void TGClient::indexChat(int64_t chatid) { + requestMessages(chatid, 0, indexDoneCallback, editMessageCallback); +} + +void TGClient::getAllTextMessages(int64_t chatid, std::function)> f) { + if(!f) return; + + // only works without multithreading! + // with multithreading onDone could be processed before the last update -> segfault + auto map = new std::map(); + requestMessages(chatid, 0, [this, map, f](int64_t chat) { + f(*map); + delete map; + }, [this, map](objptr msg) { + // try to get text + bool isText = false; + // text does not need to be freed, because it has a owning pointer in msg + td_api::formattedText* text = getMessageFormattedText(*msg, isText); + if(msg->sender_id_->get_id() == td_api::messageSenderUser::ID) { + auto sender = (td_api::messageSenderUser*) (msg->sender_id_).get(); + if(sender->user_id_ != me) { + Log::warn << "ignore non-me-config-message from: " << sender->user_id_; + return; + } + if(text) { + (*map)[msg->id_] = text->text_; + } + } + }); +} + +void TGClient::downloadFile(int32_t file_id) { + Log::info << "start download for file: " << file_id; + send_staticquery(td::make_tl_object(file_id, /* prio */ 15, 0, 0, false), HANDLER_NULL); +} + +void TGClient::sendTextMessageToSelf(const std::string& text) { + auto sendmsg = td::make_tl_object(); + sendmsg->chat_id_ = me; + sendmsg->message_thread_id_ = 0; + sendmsg->reply_to_ = nullptr; + sendmsg->options_ = nullptr; + sendmsg->reply_markup_ = nullptr; + std::vector> entities; + sendmsg->input_message_content_ = createMessageInputTextEntities(text, entities); + send_staticquery(std::move(sendmsg), HANDLER_NULL); +} + +void TGClient::getMessage(int64_t chatid, int64_t messageid, std::function)> f) { + send_wrappedquery(td::make_tl_object(chatid, messageid), f); +} + +void TGClient::setDraft(int64_t chat, const std::string& text, int64_t replyto, int64_t messageThread) { + auto itext = createMessageInputText(text); + objptr dmsg = td_api::make_object(replyto, time(nullptr), std::move(itext)); + send_staticquery(td_api::make_object(chat, messageThread, std::move(dmsg)), HANDLER_NULL); + drafts.erase(chat); //if there was a user draft, its now gone +} + +void TGClient::clearDraft(int64_t chat, int64_t messageThread) { + objptr dmsg; + send_staticquery(td_api::make_object(chat, messageThread, std::move(dmsg)), HANDLER_NULL); + drafts.erase(chat); +} + +bool TGClient::hasDraft(int64_t chat) const { + return drafts.find(chat) != drafts.end(); +} + +const User* TGClient::getCachedUser(int64_t userid) { + auto it = usercache.find(userid); + if(it == usercache.end()) { + // no user found + return nullptr; + } + + //update accesstimes + accessUser(it); + return &it->second; +} + +void TGClient::accessUser(std::map::iterator it) { + //change last access time + uint64_t lastacc = it->second.lastaccessed; + + //remove old entry + auto it2 = usercache_timeout.lower_bound(lastacc); + auto it2end = usercache_timeout.upper_bound(lastacc); + for( ;it2 != it2end; ++it2) { + if(it2->second == it->first) { + usercache_timeout.erase(it2); + break; + } + } + + uint64_t newacc = time(0); + + //insert new value + usercache_timeout.insert({newacc, it->first}); + it->second.lastaccessed = newacc; +} + +void TGClient::addUser(CachedUser& u) { + //check for existing user + auto it = usercache.find(u.tgid); + if(it != usercache.end()) { + //remove old user + removeUser(it); + } + + //add user + uint64_t accesstime = time(0); + u.lastaccessed = accesstime; + usercache.insert({u.tgid, u}); + + //add timing information + usercache_timeout.insert({accesstime, u.tgid}); + + //remove old entrys + checkUserCache(); +} + +void TGClient::removeUser(std::map::iterator it) { + if(it == usercache.end()) return; + + uint64_t accesstime = it->second.lastaccessed; + int64_t id = it->second.tgid; + + //remove from timeout cache + auto it2 = usercache_timeout.lower_bound(accesstime); + auto it2end = usercache_timeout.upper_bound(accesstime); + for( ;it2 != it2end; ++it2) { + if(it2->second == id) { + usercache_timeout.erase(it2); + break; + } + } + + //remove from real cache + usercache.erase(it); +} + +void TGClient::checkUserCache() { + if(usercache.size() > MAXCACHEDUSERCOUNT) { + uint64_t lastallowed = time(0) - CACHEDUSERCOUNTCLEANUPTIME; + auto it = usercache_timeout.begin(); + auto itend = usercache_timeout.upper_bound(lastallowed); + while(it != itend) { + if(it->first == (uint64_t) me) { + ++it; + continue; // never remove self from cache + } + + auto itcopy = it; + ++itcopy; + auto todeleteit = usercache.find(it->second); + removeUser(todeleteit); + it = itcopy; + } + } +} + +void TGClient::requestMessages(int64_t chatid, int64_t from_message_id, std::function onDone, std::function)> forMessage) { + Log::debug << "requestMessages " << chatid << " from: " << from_message_id; + send_wrappedquery(td_api::make_object(chatid, from_message_id, 0, 100, false), [this, chatid, onDone, forMessage](objptr m) { + //request next chunk + td_api::array>& arr = m->messages_; + + Log::trace << "got " << arr.size() << " messages"; + + if(arr.size() == 0) { + // indexing done + Log::info << "Chatindex done"; + if(onDone) { + onDone(chatid); + } + return; + } + + //issue next request bevore processing -> requires reverse iteration to find the last message id + int64_t smallestid = std::numeric_limits::max(); + for(td_api::array>::const_reverse_iterator it = arr.rbegin(); it != arr.rend(); ++it) { + const td_api::object_ptr& obj = *it; + if(obj) { + int64_t id = obj->id_; + if(id < smallestid) { + smallestid = id; + break; //the first element should be good, because they should be ordered + } + } + } + + //start next request + if(shouldrun && smallestid != std::numeric_limits::max()) { + requestMessages(chatid, smallestid, onDone, forMessage); + } + + //process the messages + if(forMessage) { + for(auto it = arr.begin(); it != arr.end(); ++it) { + if(*it) { + forMessage(std::move(*it)); + } + } + } + }); +} + +void TGClient::setOption(const std::string& name, objptr val) { + client_manager_->execute(td_api::make_object(name, std::move(val))); +} + +void TGClient::setOptions() { + setOption("disable_persistent_network_statistics", td_api::make_object(true)); + setOption("disable_sent_scheduled_message_notifications", td_api::make_object(true)); + setOption("disable_time_adjustment_protection", td_api::make_object(true)); + setOption("disable_top_chats", td_api::make_object(true)); + setOption("ignore_background_updates", td_api::make_object(false)); + setOption("ignore_inline_thumbnails", td_api::make_object(true)); + setOption("ignore_platform_restrictions", td_api::make_object(true)); +} + +void TGClient::loadInit() { + send_wrappedquery(td_api::make_object(), [this](objptr meu) { + CachedUser cachedme; + convertUser(*meu, cachedme); + me = cachedme.tgid; + addUser(cachedme); + + //load chat list + td_api::object_ptr mainlist = td_api::make_object(); + send_staticquery(td_api::make_object(std::move(mainlist), std::numeric_limits::max()), 2); + }); +} + +void TGClient::loadInitInternalCallback(Object o) { + (void) o; + Log::note << "init done, chats loaded"; + if(initDoneCallback) + initDoneCallback(); + initDone = true; +} + +void TGClient::restart() { + client_manager_.reset(); + authorization_state_.reset(); + + (this)->~TGClient(); + new (this) TGClient(); +} + +void TGClient::send_query(td_api::object_ptr f, std::function handler) { + auto query_id = next_query_id(); + if (handler) { + handlers_.emplace(query_id, std::move(handler)); + } + client_manager_->send(client_id_, query_id, std::move(f)); +} + +void TGClient::send_staticquery(td_api::object_ptr f, uint64_t handlerid) { + assert(handlerid < STATICHANDLERCOUNT); + client_manager_->send(client_id_, handlerid, std::move(f)); +} + +template +void TGClient::send_wrappedquery(td_api::object_ptr f, std::function)> handler, std::function onError, bool printError) { + send_query(std::move(f), [handler, onError, printError](Object o) { + if(catchErrors(o, onError, printError) && handler) { + auto casted = td::move_tl_object_as(o); + handler(std::move(casted)); + } + }); +} + +template +bool TGClient::catchErrors(const Object& o, std::function onError, bool printError) { + if(!o) return false; + if(o->get_id() == td_api::error::ID) { + if(printError) { + const objptr& err = (const objptr&) o; + Log::warn << "error: " << err->code_ << " " << err->message_; + } + if(onError) + onError(); + return false; + } + if(T::ID != Placeholder::ID && o->get_id() != T::ID) { + if(printError) + Log::warn << "function did not return required type: returned_id: " << o->get_id() << " required_id: " << T::ID; + if(onError) + onError(); + return false; + } + + return true; +} + +template +void TGClient::send_inplace(Args... args, std::function)> handler, bool printError) { + send_wrappedquery(td_api::make_object(std::forward(args)...), handler, printError); +} + +void TGClient::process_response(td::ClientManager::Response response) { + if (!response.object) { + return; + } + + // update + if (response.request_id == 0) { + return process_update(std::move(response.object)); + } + + if(response.request_id < STATICHANDLERCOUNT) { + auto handler = STATICHANDLERS[response.request_id]; + if(!catchErrors(response.object) || !handler) return; // nullptr handler + + (this->*handler)(std::move(response.object)); + return; + } + + auto it = handlers_.find(response.request_id); + if (it != handlers_.end()) { + it->second(std::move(response.object)); + handlers_.erase(it); + } +} + +void TGClient::process_update(td_api::object_ptr update) { + td_api::downcast_call( + *update, overloaded( + [this](td_api::updateAuthorizationState& update_authorization_state) { + authorization_state_ = std::move(update_authorization_state.authorization_state_); + on_authorization_state_update(); + }, + [this](td_api::updateNewMessage& update_new_message) { + if(messageCallback) { + messageCallback(std::move(update_new_message.message_)); + } + }, + [this](td_api::updateUserStatus& updateStatus) { + if(statusCallback) + statusCallback(updateStatus); + }, + [this](td_api::updateChatDraftMessage& draft) { + if(draftCallback) { + const objptr& draftmsg = draft.draft_message_; + std::string text; + if(draftmsg) { + const objptr& imc = draftmsg->input_message_text_; + if(imc->get_id() == td_api::inputMessageText::ID) { + text = ((const td_api::inputMessageText&) *imc).text_->text_; + } + } + + draftCallback(draft.chat_id_, text); + } + }, + [this](td_api::updateDeleteMessages& dele) { + if(deleteCallback && dele.is_permanent_ && !dele.from_cache_) { + int64_t chatid = dele.chat_id_; + const std::vector& list = dele.message_ids_; + + //trigger callback + for(int64_t msgid : list) { + deleteCallback(chatid, msgid); + } + } + }, + [this](td_api::updateMessageEdited& edit) { + if(editMessageCallback) { + //request full message + send_wrappedquery(td_api::make_object(edit.chat_id_, edit.message_id_), editMessageCallback); + } + }, + [this](td_api::updateChatAction& action) { + if(actionCallback) { + int64_t chatid = action.chat_id_, userid = action.chat_id_; + ChatAction::ChatAction act = ChatAction::getAction(action.action_->get_id()); + actionCallback(chatid, userid, act); + } + }, + [this](td_api::updateUser& user) { + if(userupdCallback) { + const td_api::user& ou = *user.user_; + CachedUser u; + convertUser(ou, u); + + //cache user + addUser(u); + + //trigger event + userupdCallback(u); + } + }, + [this](td_api::updateFile& fileupd) { + if(fileUpdateCallback) { + fileUpdateCallback(std::move(fileupd.file_)); + } + }, + [this](td_api::updateNewChat& newChat) { + if(newChatCallback) { + newChatCallback(std::move(newChat.chat_)); + } + }, + [this](td_api::updateChatFolders& chatFilters) { + if(chatFiltersCallback) { + std::map out; + auto& arr = chatFilters.chat_folders_; + for(auto& it : arr) { + out[it->id_] = it->title_; + } + + chatFiltersCallback(out); + } + }, + + [](auto& u) {})); //default + +} + +auto TGClient::create_authentication_query_handler() { + return [this, id = authentication_query_id_](Object object) { + if (id == authentication_query_id_) { + check_authentication_error(std::move(object)); + } + }; +} + +void TGClient::on_authorization_state_update() { + Log::trace << "update auth state " << authorization_state_->get_id(); + authentication_query_id_++; + td_api::downcast_call( + *authorization_state_, + overloaded( + [this](td_api::authorizationStateReady&) { + are_authorized_ = true; + Log::note << "Got authorization"; + loadInit(); + }, + [this](td_api::authorizationStateLoggingOut&) { + are_authorized_ = false; + std::cout << "Logging out" << std::endl; + }, + [this](td_api::authorizationStateClosing&) { std::cout << "Closing" << std::endl; }, + [this](td_api::authorizationStateClosed&) { + are_authorized_ = false; + need_restart_ = true; + std::cout << "Terminated" << std::endl; + }, + [this](td_api::authorizationStateWaitCode&) { + + if(!getAuthCodeCallback) { + Log::error << "no authCode Callback registered"; + return; + } + + std::string code = getAuthCodeCallback(); + send_query(td_api::make_object(code), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitRegistration&) { + std::string first_name; + std::string last_name; + std::cout << "Enter your first name: " << std::flush; + std::cin >> first_name; + std::cout << "Enter your last name: " << std::flush; + std::cin >> last_name; + send_query(td_api::make_object(first_name, last_name), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitPassword&) { + send_query(td_api::make_object(Config::config.tgCloudPassword), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitOtherDeviceConfirmation& state) { + std::cout << "Confirm this login link on another device: " << state.link_ << std::endl; + }, + [this](td_api::authorizationStateWaitPhoneNumber&) { + if(Config::config.phoneNumber.empty()) { + Log::fatal << "empty phone number"; + return; + } + + send_query(td_api::make_object(Config::config.phoneNumber, nullptr), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitEmailAddress&) { + send_query(td_api::make_object(""), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitEmailCode&) { + send_query(td_api::make_object(""), + create_authentication_query_handler()); + }, + [this](td_api::authorizationStateWaitTdlibParameters&) { + auto parameters = td_api::make_object(); + parameters->database_directory_ = "tdlib"; + parameters->use_test_dc_ = false; + + parameters->use_file_database_ = true; + parameters->use_chat_info_database_ = true; + parameters->use_message_database_ = true; + + parameters->use_secret_chats_ = false; + parameters->ignore_file_names_ = false; + + parameters->api_id_ = Config::config.apiid; + parameters->api_hash_ = Config::config.apihash; + Log::info << "using api: " << Config::config.apiid << " " << Config::config.apihash; + parameters->system_language_code_ = "en"; + parameters->device_model_ = "Desktop"; + parameters->application_version_ = "1.0"; + parameters->enable_storage_optimizer_ = true; + send_query(std::move(parameters), + create_authentication_query_handler()); + setOptions(); + })); +} + +void TGClient::check_authentication_error(Object object) { + if (object->get_id() == td_api::error::ID) { + auto error = td::move_tl_object_as(object); + std::cout << "Error: " << to_string(error) << std::flush; + on_authorization_state_update(); + } +} + +std::uint64_t TGClient::next_query_id() { + return ++current_query_id_; +} \ No newline at end of file diff --git a/src/tgtui.cpp b/src/tgtui.cpp new file mode 100644 index 0000000..3eb51bb --- /dev/null +++ b/src/tgtui.cpp @@ -0,0 +1,110 @@ +#include "tgtui.h" + +#include + +#include + +#include "config.h" +#include "view.h" +#include "viewchatlist.h" + +namespace pl = std::placeholders; + +TgTUI::TgTUI() : tgclient(std::bind(&TgTUI::initDoneCB, this)) { + views.push_back(new ViewChatList(*this)); +} + +TgTUI::~TgTUI() { + for(View* v : views) { + delete v; + } + views.clear(); +} + +void TgTUI::run() { + + //apply config + tgclient.setAuthData( [](){ + /* + std::cout << "Auth Code: " << std::flush; + std::string code; + std::cin >> code; // TODO: take input different + return code; + */ + return ""; + }); + + //register callbacks + /* + tgclient.registerDraftHandler(std::bind(&UserBot::handleDraft, this, pl::_1, pl::_2)); + tgclient.registerActionHandler(std::bind(&UserBot::handleAction, this, pl::_1, pl::_2, pl::_3)); + tgclient.registerDeleteHandler(std::bind(&UserBot::handleDelete, this, pl::_1, pl::_2)); + tgclient.registerStatusUpdateHandler(std::bind(&UserBot::handleStatus, this, pl::_1)); + tgclient.registerEditMessageHandler(std::bind(&UserBot::handleEditMessage, this, pl::_1)); + tgclient.registerNewMessageHandler(std::bind(&UserBot::handleMessage, this, pl::_1)); + tgclient.registerUserUpdateHandler(std::bind(&Store::logUser, store, pl::_1)); + tgclient.registerIndexDoneHandle(std::bind(&UserBot::handleIndexDone, this, pl::_1)); + tgclient.registerFileUpdateCallback(std::bind(&UserBot::handleFileUpdate, this, pl::_1)); + tgclient.registerChatFiltersCallback(std::bind(&UserBot::handleChatFilters, this, pl::_1)); + */ + + tgclient.registerNewChatCallback(std::bind(&TgTUI::handleNewChat, this, pl::_1)); + + //run client + tgclient.loop(); +} + +void TgTUI::stop() { + shouldRun = false; + + tgclient.stop(); + + if(tuiThread.joinable()) { + tuiThread.join(); + } +} + +const std::vector& TgTUI::getChats() { + return chats; +} + +void TgTUI::initDoneCB() { + shouldRun = true; + currentView = views.at(0); + tuiThread = std::thread(&TgTUI::threadLoop, this); +} + +void TgTUI::threadLoop() { + // init ncurses + ::initscr(); + // ::start_color(); + ::cbreak(); // Disable line buffering + ::keypad(stdscr, TRUE); // Enable special keys like arrow keys + ::noecho(); // Don't print characters to the screen + + currentView->open(); + + bool keep = true; + while(shouldRun && keep) { + ::clear(); + + currentView->paint(); + + ::refresh(); + + int ch = getch(); + + keep = currentView->keyIn(ch); + } + + currentView->close(); + + // deinit ncurses + ::nocbreak(); + ::echo(); + ::endwin(); +} + +void TgTUI::handleNewChat(objptr chat) { + chats.push_back({chat->id_, chat->title_}); +} diff --git a/src/typeconverter.cpp b/src/typeconverter.cpp new file mode 100644 index 0000000..c39be4f --- /dev/null +++ b/src/typeconverter.cpp @@ -0,0 +1,270 @@ +#include "typeconverter.h" +#include "message.h" + +//converts between tdlib types and own types + +ChatType convertChatType(const td_api::ChatType& in) { + switch(in.get_id()) { + case td_api::chatTypeBasicGroup::ID: + return ChatType::GROUP; + case td_api::chatTypePrivate::ID: + return ChatType::PRIVATE; + case td_api::chatTypeSecret::ID: + return ChatType::SECRET; + case td_api::chatTypeSupergroup::ID: { + if(((const td_api::chatTypeSupergroup&) in).is_channel_) + return ChatType::CHANNEL; + return ChatType::SUPERGROUP; + } + } + return ChatType::UNKNOWN; //unknown +} + +void convertUser(const td_api::user& in, User& out) { + if(in.usernames_ && in.usernames_->active_usernames_.empty()) { + out.username = in.usernames_->active_usernames_.at(0); + } else { + out.username = {}; + } + + out.tgid = in.id_; + out.firstname = in.first_name_; + out.lastname = in.last_name_; + out.phonenumber = in.phone_number_; + out.isContact = in.is_contact_; + out.isMutalContact = in.is_mutual_contact_; + out.isVerified = in.is_verified_; + out.isSupport = in.is_support_; + out.restriction = in.restriction_reason_; + out.isScam = in.is_scam_; + out.haveAccess = in.have_access_; + out.type = convertUserType(*in.type_); +} + +UserType convertUserType(const td_api::UserType& in) { + switch(in.get_id()) { + case td_api::userTypeBot::ID: + return UserType::BOT; + case td_api::userTypeDeleted::ID: + return UserType::DELETED; + case td_api::userTypeRegular::ID: + return UserType::REGULAR; + } + return UserType::UNKNOWN; //unknown +} + +const std::string& convertUserType(UserType type) { + switch(type) { + case UserType::BOT: + return UserTypeTypes[0]; + case UserType::DELETED: + return UserTypeTypes[1]; + case UserType::REGULAR: + return UserTypeTypes[2]; + default: break; + } + return UserTypeTypes[3]; +} + +UserType convertUserType(const std::string& in) { + if(in == UserTypeTypes[0]) + return UserType::BOT; + if(in == UserTypeTypes[1]) + return UserType::DELETED; + if(in == UserTypeTypes[2]) + return UserType::REGULAR; + return UserType::UNKNOWN; +} + +const std::string& convertMessageType(MessageType type) { + uint32_t i = (uint32_t) type; + if(i > 14) return MessageTypeTypes[0]; //unknown + return MessageTypeTypes[i]; +} + +MessageType convertMessageType(const std::string& in) { + for(int32_t i = 0; i < 14; ++i) { + if(MessageTypeTypes[i] == in) { + return (MessageType) i; + } + } + return MessageType::UNKNOWN; +} + +static int64_t getSender(const td_api::MessageSender& s) { + if(s.get_id() == td_api::messageSenderUser::ID) { + return static_cast(s).user_id_; + } + if(s.get_id() == td_api::messageSenderChat::ID) { + return static_cast(s).chat_id_; + } + return 0; +} + +void convertMessage(const td_api::message& in, Message& out) { + out.id = in.id_; + out.chat = in.chat_id_; + out.sender = getSender(*in.sender_id_); + out.text = ""; + out.type = convertMessageTypeandText(*in.content_, out.text); + out.isDeleted = false; + out.isOutgoing = in.is_outgoing_; + out.isPinned = in.is_pinned_; + out.isEditable = in.can_be_edited_; + out.isForwardable = in.can_be_forwarded_; + out.sendDate = in.date_; + out.editDate = in.edit_date_; + out.messageThreadId = in.message_thread_id_; + out.viaBotid = in.via_bot_user_id_; + out.mediaAlbumId = in.media_album_id_; + + ReplyMarkup* markup = convertReplyMarkup(in.reply_markup_.get()); + if(markup) { + out.replyMarkup = std::shared_ptr(markup); + } +} + + +#define CHECKCAST(TYPE, MTYPE, ACCESS) case td_api::TYPE::ID: \ + textout = static_cast(cont).ACCESS; \ + return MessageType::MTYPE +#define ECHECKCAST(TYPE, MTYPE) case td_api::TYPE::ID: \ + return MessageType::MTYPE + +MessageType convertMessageTypeandText(const td_api::MessageContent& cont, std::string& textout) { + switch(cont.get_id()) { + CHECKCAST(messageText, TEXT, text_->text_); + + CHECKCAST(messageAnimation, ANIMATION, caption_->text_); + CHECKCAST(messageAudio, AUDIO, caption_->text_); + CHECKCAST(messageContact, CONTACT, contact_->first_name_); + CHECKCAST(messageDocument, DOCUMENT, caption_->text_); + ECHECKCAST(messageLocation, LOCATION); + CHECKCAST(messagePhoto, PHOTO, caption_->text_); + ECHECKCAST(messageExpiredPhoto, PHOTO); + CHECKCAST(messagePoll, POLL, poll_->question_); + ECHECKCAST(messageSticker, STICKER); + ECHECKCAST(messageDice, STICKER); + CHECKCAST(messageVideo, VIDEO, caption_->text_); + ECHECKCAST(messageExpiredVideo, VIDEO); + ECHECKCAST(messageVideoNote, VIDEO); + CHECKCAST(messageVenue, VENUE, venue_->title_); + CHECKCAST(messageVoiceNote, VOICE, caption_->text_); + ECHECKCAST(messageUnsupported, UNSUPPORTED); + + default: + return MessageType::SERVICE; + + } + // unreachable + return MessageType::UNKNOWN; +} + +#undef CHECKCAST +#undef ECHECKCAST + +ReplyMarkup* convertReplyMarkup(const td_api::ReplyMarkup* in) { + if(!in) + return nullptr; + if(in->get_id() == td_api::replyMarkupInlineKeyboard::ID) { + const td_api::replyMarkupInlineKeyboard* kyb = (const td_api::replyMarkupInlineKeyboard*) in; + InlineReplyMarkup* out = new InlineReplyMarkup(); + + // convertbuttons + for(uint32_t i = 0; i < kyb->rows_.size(); ++i) { + const std::vector>& row = kyb->rows_.at(i); + for(uint32_t j = 0; j < row.size(); ++j) { + InlineButton* btn = convertInlineButton(row.at(j).get()); + if(btn) + out->buttons.push_back(btn); + } + } + + return out; + } + return nullptr; +} + +InlineButton* convertInlineButton(const td_api::inlineKeyboardButton* in) { + //construct button + InlineButton* btn = nullptr; + const td_api::InlineKeyboardButtonType* type = in->type_.get(); + if(type->get_id() == td_api::inlineKeyboardButtonTypeCallback::ID) { + btn = new InlineCallbackButton(); + ((InlineCallbackButton*) btn)->data = ((const td_api::inlineKeyboardButtonTypeCallback*) type)->data_; + } else if(type->get_id() == td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID) { + btn = new InlineCallbackPasswordButton(); + ((InlineCallbackPasswordButton*) btn)->data = ((const td_api::inlineKeyboardButtonTypeCallbackWithPassword*) type)->data_; + } else if(type->get_id() == td_api::inlineKeyboardButtonTypeLoginUrl::ID) { + InlineLoginURLButton* lurlbtn = new InlineLoginURLButton(); + const td_api::inlineKeyboardButtonTypeLoginUrl* casted = (const td_api::inlineKeyboardButtonTypeLoginUrl*) type; + lurlbtn->url = casted->url_; + lurlbtn->id = casted->id_; + lurlbtn->forwardText = casted->forward_text_; + btn = lurlbtn; + } else if(type->get_id() == td_api::inlineKeyboardButtonTypeSwitchInline::ID) { + InlineSwitchInlineButton* lsbtn = new InlineSwitchInlineButton(); + const td_api::inlineKeyboardButtonTypeSwitchInline* casted = (const td_api::inlineKeyboardButtonTypeSwitchInline*) type; + lsbtn->query = casted->query_; + lsbtn->inCurrentChat = (casted->target_chat_->get_id() == td_api::targetChatCurrent::ID); + btn = lsbtn; + } else if(type->get_id() == td_api::inlineKeyboardButtonTypeUrl::ID) { + btn = new InlineURLButton(); + ((InlineURLButton*) btn)->url = ((const td_api::inlineKeyboardButtonTypeUrl*) type)->url_; + } + + if(btn) { + btn->text = in->text_; + } + return btn; +} + +bool User::operator==(const User& u) const { + return (tgid == u.tgid && firstname == u.firstname && lastname == u.lastname && username == u.username && phonenumber == u.phonenumber && isContact == u.isContact && isMutalContact == u.isMutalContact && isVerified == u.isVerified && isSupport == u.isSupport && restriction == u.restriction && isScam == u.isScam && haveAccess == u.haveAccess && type == u.type); +} + +bool User::operator!=(const User& u) const { + return !operator==(u); +} + +bool Message::operator==(const Message& m) const { + return (id == m.id && chat == m.chat && sender == m.sender && type == m.type && text == m.text && isDeleted == m.isDeleted && isOutgoing == m.isOutgoing && isPinned == m.isPinned && isForwardable == m.isForwardable && sendDate == m.sendDate && editDate == m.sendDate && viaBotid == m.viaBotid && mediaAlbumId == m.mediaAlbumId); +} + +bool Message::operator!=(const Message& m) const { + return !operator==(m); +} + +const std::string& getMessageText(const td_api::object_ptr& msg) { + if(msg->content_->get_id() == td_api::messageText::ID) { + const std::string& text = ((const td_api::messageText&) *msg->content_).text_->text_; + return text; + } + static const std::string EMPTY = ""; + return EMPTY; +} + + +#define CHECKCAST(TYPE) case td_api::TYPE::ID: \ + return &*static_cast(cont).caption_; +td_api::formattedText* getMessageFormattedText(td_api::message& msg, bool& isText) { + if(!msg.content_) return nullptr; + + td_api::MessageContent& cont = *msg.content_; + isText = false; + switch(cont.get_id()) { + CHECKCAST(messageAnimation); + CHECKCAST(messageAudio); + CHECKCAST(messageDocument); + CHECKCAST(messagePhoto); + CHECKCAST(messageVideo); + CHECKCAST(messageVoiceNote); + case td_api::messageText::ID: + isText = true; + return &*static_cast(cont).text_; + } + + //default + return nullptr; +} +#undef CHECKCAST \ No newline at end of file diff --git a/src/userstatus.cpp b/src/userstatus.cpp new file mode 100644 index 0000000..d07bf66 --- /dev/null +++ b/src/userstatus.cpp @@ -0,0 +1,49 @@ +#include "userstatus.h" + +#include +namespace td_api = td::td_api; + +#define CASERETURN(TGTYPE, ENUMTYPE) case td_api::TGTYPE::ID: return UserStatus::ENUMTYPE +UserStatus::UserStatus UserStatus::getStatus(int32_t status) { + switch(status) { + CASERETURN(userStatusEmpty , EMPTY); + CASERETURN(userStatusOnline , ONLINE); + CASERETURN(userStatusOffline , OFFLINE); + CASERETURN(userStatusRecently , RECENTLY); + CASERETURN(userStatusLastWeek , LASTWEEK); + CASERETURN(userStatusLastMonth, LASTMONTH); + } + return UserStatus::EMPTY; +} +#undef CASERETURN + +const std::string& UserStatus::statustoString(UserStatus status) { + static const std::string names[] {"empty", "online", "offline", "recently", "lastweek", "lastmonth"}; + return names[(uint8_t) status]; +} + +#define CASERETURN(TGTYPE, ENUMTYPE) case td_api::TGTYPE::ID: return ChatAction::ENUMTYPE +ChatAction::ChatAction ChatAction::getAction(int32_t status) { + switch(status) { + CASERETURN(chatActionCancel , CANCEL); + CASERETURN(chatActionChoosingContact , CHOOSINGCONTACT); + CASERETURN(chatActionChoosingLocation , CHOOSINGLOCATION); + CASERETURN(chatActionRecordingVideo , RECORDINGVIDEO); + CASERETURN(chatActionRecordingVideoNote, RECORDINGVIDEONOTE); + CASERETURN(chatActionRecordingVoiceNote, RECORDINGVOICENOTE); + CASERETURN(chatActionStartPlayingGame , PLAYGAME); + CASERETURN(chatActionTyping , TYPING); + CASERETURN(chatActionUploadingDocument , UPLOADINGDOCUMENT); + CASERETURN(chatActionUploadingPhoto , UPLOADINGPHOTO); + CASERETURN(chatActionUploadingVideo , UPLODAINGVIDEO); + CASERETURN(chatActionUploadingVideoNote, UPLOADINGVIDEONOTE); + CASERETURN(chatActionUploadingVoiceNote, UPLOADINGVOICENOTE); + } + return ChatAction::CANCEL; +} +#undef CASERETURN + +const std::string& ChatAction::actiontoString(ChatAction action) { + static const std::string names[] {"CANCEL", "CHOOSINGCONTACT ", "CHOOSINGLOCATION ", "RECORDINGVIDEO ", "RECORDINGVIDEONOTE ", "RECORDINGVOICENOTE ", "PLAYGAME ", "TYPING ", "UPLOADINGDOCUMENT ", "UPLOADINGPHOTO ", "UPLODAINGVIDEO ", "UPLOADINGVIDEONOTE ", "UPLOADINGVOICENOTE"}; + return names[action]; +} \ No newline at end of file diff --git a/src/view.cpp b/src/view.cpp new file mode 100644 index 0000000..f211309 --- /dev/null +++ b/src/view.cpp @@ -0,0 +1,13 @@ +#include "view.h" + +View::View(TgTUI& tgtui) : tgtui(tgtui) {} + +View::~View() {} + +void View::open() {} + +void View::close() {} + +bool View::keyIn(int) { + return true; +} diff --git a/src/viewchatlist.cpp b/src/viewchatlist.cpp new file mode 100644 index 0000000..be41353 --- /dev/null +++ b/src/viewchatlist.cpp @@ -0,0 +1,61 @@ +#include "viewchatlist.h" + +#include + +#include "tgtui.h" + +ViewChatList::ViewChatList(TgTUI& tgtui) : View(tgtui) {} + +void ViewChatList::open() { + chats = tgtui.getChats(); + currentChatOffset = 0; + selectedChatRow = 0; +} + +void ViewChatList::close() { + chats.clear(); +} + +void ViewChatList::paint() { + ::printw("Chats:\n"); + + getmaxyx(stdscr, maxRows, maxCols); + for(int row = 1; row < maxRows-1 && currentChatOffset + row < chats.size(); ++row) { + int selection = ' '; + const bool currentRowSelected = (row == selectedChatRow + currentChatOffset +1); + if(currentRowSelected) { + selection = '#'; + attron(A_REVERSE); + } + + ::mvprintw(row, 0, "%c %s\n", selection, chats.at(currentChatOffset + row -1).name.c_str()); + + if(currentRowSelected) { + attroff(A_REVERSE); + } + } +} + +bool ViewChatList::keyIn(int key) { + switch (key) { + case KEY_UP: + if (selectedChatRow > 0) { + selectedChatRow--; + } + break; + case KEY_DOWN: + if (selectedChatRow < maxRows - 2) { + selectedChatRow++; + } + break; + case KEY_RIGHT: + case KEY_ENTER: + return false; + } + + return true; +} + +int64_t ViewChatList::getSelectedChatId() { + return chats.at(selectedChatRow + currentChatOffset).chatId; +} diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..a3c436f --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,70 @@ +#include "test.h" + +#include +#include +#include +#include +#include +#include + +#define RED "\033[1;91m" +#define GREEN "\033[1;92m" +#define YELLOW "\033[1;93m" +#define AQUA "\033[1;36m" +#define GRAY "\033[1;38;5;244m" +#define RESET "\033[;1m" + + +int main(int argc, char** argv) { + const std::chrono::time_point start = std::chrono::high_resolution_clock::now(); + + testdef* startit = &__start_testlist, *endit = &__stop_testlist; + int failcount = 0; + int skipcount = 0; + int testcount = endit-startit; + int testnumber = 0; + + // get the maximum length of a test name + int testNameMaxLen = 0; + for(testdef* it = startit; it != endit; ++it) { + testNameMaxLen = std::max(testNameMaxLen, std::string(it->name).size()); + } + + // go through back -> front (tests are inserted in reverse order) + for(testdef* it = startit + testcount-1; it >= startit; --it) { + const std::string testName(it->name); + const std::string namePadding((int) (testNameMaxLen - testName.size()) + 1, ' '); + std::cout << RESET "Running test:" << std::setfill(' ') << std::setw(std::log10(testcount)+2) << ++testnumber << '/' << testcount << " " AQUA << testName << RESET << namePadding; + + // run test + int result = TESTFAILED; + const std::chrono::time_point testStart = std::chrono::high_resolution_clock::now(); + try { + result = (it->testf)(); + } catch(std::exception& e) { + std::cout << "catched exception: \"" << e.what() << "\" " << std::flush; + } catch(...) {} + const std::chrono::time_point testEnd = std::chrono::high_resolution_clock::now(); + const std::chrono::duration testDuration(testEnd-testStart); + + std::cout << std::fixed << std::setprecision(1); + if(result == TESTGOOD) { + std::cout << GREEN "succeeded" RESET "! " GRAY << testDuration.count() << "ms" RESET << std::endl; + } else if(result == TESTSKIPPED) { + std::cout << YELLOW " skipped" RESET "! " GRAY << testDuration.count() << "ms" RESET << std::endl; + skipcount++; + } else { + std::cout << RED " failed" RESET "! " GRAY << testDuration.count() << "ms" RESET << std::endl; + failcount++; + } + } + + const char* color = (failcount > 0 ? RED : GREEN); // red or green + std::cout << color << failcount << RESET "/" << testcount << " failed (" YELLOW << skipcount << RESET " skipped)" << std::endl; + + const std::chrono::time_point end = std::chrono::high_resolution_clock::now(); + const std::chrono::duration t = end - start; + std::cout << "Testing took: " << t.count() << "ms" << std::endl; + + return failcount > 0; +} diff --git a/tests/sampletest.cpp b/tests/sampletest.cpp new file mode 100644 index 0000000..b0e7b4b --- /dev/null +++ b/tests/sampletest.cpp @@ -0,0 +1,12 @@ +#include "test.h" + +#include +#include + +// tests are executed top to bottom + +TEST(ABC) { + CMPASSERT(1, true); + +} TESTEND + diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..ef1456c --- /dev/null +++ b/tests/test.h @@ -0,0 +1,43 @@ +#define TESTFAILED 0 +#define TESTGOOD 1 +#define TESTSKIPPED -1 + +#include + +#define TESTDATA "./tests/data/" + +// very helpfull: https://mgalgs.io/2013/05/10/hacking-your-ELF-for-fun-and-profit.html + +#define TESTNAME(NAME) test_##NAME +#define TESTFUNC(NAME) int TESTNAME(NAME)() + +#define REGISTERTEST(NAME) static const testdef __test_ ## NAME \ + __attribute((__section__("testlist"))) \ + __attribute((__used__)) = { \ + TESTNAME(NAME), \ + #NAME, \ + } + +#define TEST(NAME) static TESTFUNC(NAME); \ + REGISTERTEST(NAME); \ + TESTFUNC(NAME) { + +#define TESTEND return TESTGOOD; } \ + + +#define ASSERT(BED, ERR) if(!(BED)) { std::cout << __FILE__ << ":" << __LINE__ << " " << ERR << ' ' << std::flush; return TESTFAILED; } +#define CMPASSERTE(IS, SHOULD, ERR) if( !((IS) == (SHOULD))) { std::cout << __FILE__ << ":" << __LINE__ << " is: \"" << (IS) << "\" should: \"" << (SHOULD) << "\" "<< std::flush; return TESTFAILED; } +#define CMPASSERT(IS, SHOULD) CMPASSERTE(IS, SHOULD, "") + +#define SKIPTEST return TESTSKIPPED + +typedef int (*test_t)(); + +struct testdef { + test_t testf; + const char* name; +}; + +// linker generates this <3 +extern struct testdef __start_testlist; +extern struct testdef __stop_testlist; \ No newline at end of file diff --git a/thirdparty/Log b/thirdparty/Log new file mode 160000 index 0000000..c5274a5 --- /dev/null +++ b/thirdparty/Log @@ -0,0 +1 @@ +Subproject commit c5274a56b8a306fd184e80bd023824e623c56c27 diff --git a/thirdparty/td b/thirdparty/td new file mode 160000 index 0000000..00258cc --- /dev/null +++ b/thirdparty/td @@ -0,0 +1 @@ +Subproject commit 00258ccb4cef19e4d6b4392082c68755887d9c2f