This commit is contained in:
mrbesen 2023-11-20 19:57:37 +01:00
commit d5592d5ee3
Signed by: MrBesen
GPG Key ID: 596B2350DCD67504
33 changed files with 2319 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@ -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

6
.gitmodules vendored Normal file
View File

@ -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

20
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -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
}

50
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

31
.vscode/tasks.json vendored Normal file
View File

@ -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"],
}
]
}

85
Makefile Normal file
View File

@ -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)

67
inc/chat.h Normal file
View File

@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <string>
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;
}

18
inc/config.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <string>
struct Config {
Config();
int apiid;
std::string apihash;
std::string phoneNumber;
std::string tgCloudPassword;
static Config config;
};

55
inc/message.h Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include <cstdint>
#include <string>
#include <memory>
#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> replyMarkup;
bool operator==(const Message&) const;
bool operator!=(const Message&) const;
};

7
inc/msgident.h Normal file
View File

@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
struct MsgIdent {
int64_t chat;
int64_t msgid;
};

91
inc/replymarkup.h Normal file
View File

@ -0,0 +1,91 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <ostream>
// 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<InlineButton*> buttons;
};

9
inc/slimchat.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <cstdint>
#include <string>
struct SlimChat {
int64_t chatId;
std::string name;
};

180
inc/tgclient.h Normal file
View File

@ -0,0 +1,180 @@
#pragma once
#include <td/telegram/Client.h>
#include <td/telegram/td_api.h>
#include <atomic>
#include <chat.h>
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <set>
#include "userstatus.h"
#include "user.h"
namespace td_api = td::td_api;
using Object = td_api::object_ptr<td_api::Object>;
template<typename T>
using objptr = td_api::object_ptr<T>;
using MessageCallback = std::function<void(objptr<td_api::message>)>;
using StatusCallback = std::function<void(const td_api::updateUserStatus&)>;
using DraftCallback = std::function<void(int64_t chatid, const std::string&)>;
using ActionCallback = std::function<void(int64_t chatid, int64_t userid, ChatAction::ChatAction)>;
using DeleteCallback = std::function<void(int64_t chatid, int64_t msgid)>;
using UserUpdateCallback = std::function<void(const User&)>;
using EditMessageCallback = std::function<void(objptr<td_api::message>)>;
using IndexDoneCallback = std::function<void(int64_t)>;
using FileUpdateCallback = std::function<void(objptr<td_api::file>)>;
using NewChatCallback = std::function<void(objptr<td_api::chat>)>;
using ChatFiltersCallback = std::function<void(std::map<int32_t, std::string>)>;
class TGClient {
public:
static const uint64_t STATICHANDLERCOUNT = 128;
static void(TGClient::* STATICHANDLERS [STATICHANDLERCOUNT])(objptr<td_api::Object>);
TGClient(std::function<void()> initDoneCallback = {});
void setAuthData(std::function<std::string()> 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<td_api::message>& rplyto, const std::string& awnser);
void reply(int64_t chat, int64_t msg, const std::string& awnser);
void editMessage(const objptr<td_api::message>& toEdit, const std::string& newText);
void editMessage(const objptr<td_api::message>& toEdit, const std::string& newText, std::vector<objptr<td_api::textEntity>>& entities);
void editMessageCaption(const objptr<td_api::message>& toEdit, const std::string& newText, std::vector<objptr<td_api::textEntity>>& 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(std::map<int64_t, std::string>)>);
void downloadFile(int32_t file_id);
void sendTextMessageToSelf(const std::string& text);
void getMessage(int64_t chatid, int64_t messageid, std::function<void(objptr<td_api::message>)> 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<td::ClientManager> client_manager_;
std::int32_t client_id_{0};
td_api::object_ptr<td_api::AuthorizationState> 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<std::uint64_t, std::function<void(Object)>> handlers_; //handler list maps updateid -> callback
std::map<int64_t, objptr<td_api::draftMessage>> 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<int64_t, CachedUser> usercache; //list of cached users
std::multimap<uint64_t, int64_t> 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<void()> initDoneCallback;
//callbacks
std::function<std::string()> 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<int64_t, CachedUser>::iterator it); //iterator to cached user object in usercache
void addUser(CachedUser& u);
void removeUser(std::map<int64_t, CachedUser>::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<void(int64_t)> onDone = {}, std::function<void(td_api::object_ptr<td_api::message>)> forMessage = {});
//helper
void iterate(const td_api::array<td_api::object_ptr<td_api::message>>& messages, std::function<void(const td_api::message&)> handler);
void setOption(const std::string& name, objptr<td_api::OptionValue> 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<td_api::Function> f, std::function<void(Object)> handler = {});
void send_staticquery(td_api::object_ptr<td_api::Function> f, uint64_t handlerid);
template<typename T>
void send_wrappedquery(td_api::object_ptr<td_api::Function> f, std::function<void(td::tl_object_ptr<T>)> handler = {}, std::function<void()> onError = {}, bool printError = true);
template<typename T>
static bool catchErrors(const Object& o, std::function<void()> onError = {}, bool printError = true);
template<typename RequestType, typename ResponseType, typename... Args>
void send_inplace(Args... args, std::function<void(td::tl_object_ptr<ResponseType>)> handler = {}, bool printError = true);
void process_response(td::ClientManager::Response response);
void process_update(td_api::object_ptr<td_api::Object> update);
auto create_authentication_query_handler();
void on_authorization_state_update();
void check_authentication_error(Object object);
std::uint64_t next_query_id();
};

36
inc/tgtui.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <thread>
#include "slimchat.h"
#include "tgclient.h"
class View;
class TgTUI {
public:
TgTUI();
~TgTUI();
void run();
void stop();
const std::vector<SlimChat>& getChats();
private:
void initDoneCB();
void threadLoop();
void handleNewChat(objptr<td_api::chat> chat);
TGClient tgclient;
View* currentView = nullptr;
std::vector<View*> views;
std::vector<SlimChat> chats;
bool shouldRun = false;
std::thread tuiThread;
};

26
inc/typeconverter.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <td/telegram/td_api.h>
#include "chat.h"
#include "message.h"
#include "user.h"
namespace td_api = td::td_api;
using Object = td_api::object_ptr<td_api::Object>;
//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<td_api::message>& msg);
td_api::formattedText* getMessageFormattedText(td_api::message& msg, bool& isText);

36
inc/user.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <cstdint>
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;
};

43
inc/userstatus.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <cstdint>
#include <string>
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);
}

19
inc/view.h Normal file
View File

@ -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;
};

28
inc/viewchatlist.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <vector>
#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<SlimChat> chats;
int32_t currentChatOffset = 0;
int32_t selectedChatRow = 0;
int maxRows, maxCols;
};

11
src/config.cpp Normal file
View File

@ -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("")
{
}

35
src/main.cpp Normal file
View File

@ -0,0 +1,35 @@
#include <Log.h>
#include <fstream>
#include <signal.h>
#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;
}

55
src/replymarkup.cpp Normal file
View File

@ -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;
}
}

761
src/tgclient.cpp Normal file
View File

@ -0,0 +1,761 @@
#include "tgclient.h"
#include <cassert>
#include <td/telegram/td_api.hpp>
#include <iostream>
#include <sstream>
#include <Log.h>
#include "config.h"
#include "typeconverter.h"
namespace pl = std::placeholders;
// overloaded
namespace detail {
template <class... Fs>
struct overload;
template <class F>
struct overload<F> : public F {
explicit overload(F f) : F(f) { }
};
template <class F, class... Fs>
struct overload<F, Fs...>
: public overload<F>
, overload<Fs...> {
overload(F f, Fs... fs) : overload<F>(f), overload<Fs...>(fs...) { }
using overload<F>::operator();
using overload<Fs...>::operator();
};
} // namespace detail
template <class... F>
auto overloaded(F... f) {
return detail::overload<F...>(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<td_api::Object>) = {
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<void()> initDoneCallback) : initDoneCallback(initDoneCallback) {
td::ClientManager::execute(td_api::make_object<td_api::setLogVerbosityLevel>(1));
client_manager_ = std::make_unique<td::ClientManager>();
client_id_ = client_manager_->create_client_id();
send_query(td_api::make_object<td_api::getOption>("version"));
}
void TGClient::setAuthData(std::function<std::string()> 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<td_api::close>());
}
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<objptr<td_api::textEntity>>& entities) {
auto formattedtext = td::make_tl_object<td_api::formattedText>();
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<td_api::textEntity>(ref->offset_, ref->length_, std::move(ref->type_)));
}
return formattedtext;
}
static auto createMessageInputTextEntities(const std::string& in, std::vector<objptr<td_api::textEntity>>& entities) {
auto formattedText = createFormattedText(in, entities);
return td::make_tl_object<td_api::inputMessageText>(std::move(formattedText), false, false);
}
static auto createMessageInputText(const std::string& in) {
std::vector<objptr<td_api::textEntity>> entities;
return createMessageInputTextEntities(in, entities);
}
void TGClient::deleteMessage(int64_t chatid, int64_t messageid, bool forall) {
send_staticquery(td::make_tl_object<td_api::deleteMessages>(chatid, td_api::array<int64_t>(1, messageid), forall), HANDLER_NULL);
}
void TGClient::screenShottaken(int64_t chatid) {
td_api::array<td_api::int53> messageIds;
send_staticquery(td::make_tl_object<td_api::viewMessages>(chatid, std::move(messageIds), td::make_tl_object<td_api::messageSourceScreenshot>(), false), HANDLER_NULL);
}
void TGClient::reply(const objptr<td_api::message>& 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<td_api::sendMessage>();
sendMsg->chat_id_ = chat;
sendMsg->reply_to_ = td_api::make_object<td_api::messageReplyToMessage>(chat, msg);
sendMsg->input_message_content_ = std::move(inputmsgCont);
send_staticquery(std::move(sendMsg), HANDLER_NULL);
}
void TGClient::editMessage(const objptr<td_api::message>& toEdit, const std::string& newText) {
std::vector<objptr<td_api::textEntity>> entities;
editMessage(toEdit, newText, entities);
}
void TGClient::editMessage(const objptr<td_api::message>& toEdit, const std::string& newText, std::vector<objptr<td_api::textEntity>>& entities) {
auto editMsg = td::make_tl_object<td_api::editMessageText>();
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<td_api::message>& toEdit, const std::string& newText, std::vector<objptr<td_api::textEntity>>& entities ) {
auto editMsg = td::make_tl_object<td_api::editMessageCaption>();
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<td_api::callbackQueryPayloadData>();
payloadobj->data_ = payload;
auto sendCallback = td::make_tl_object<td_api::getCallbackQueryAnswer>();
sendCallback->chat_id_ = chat;
sendCallback->message_id_ = messageid;
sendCallback->payload_ = std::move(payloadobj);
send_wrappedquery<td_api::callbackQueryAnswer>(std::move(sendCallback), [](objptr<td_api::callbackQueryAnswer> 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<td_api::chatNotificationSettings>();
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<td_api::setChatNotificationSettings>(chatid, std::move(settings)), HANDLER_NULL);
}
void TGClient::addChatToChatListFilter(int64_t chatid, int32_t chatfilterid) {
send_staticquery(td::make_tl_object<td_api::addChatToList>(chatid, td::make_tl_object<td_api::chatListFolder>(chatfilterid)), HANDLER_NULL);
}
void TGClient::indexChat(int64_t chatid) {
requestMessages(chatid, 0, indexDoneCallback, editMessageCallback);
}
void TGClient::getAllTextMessages(int64_t chatid, std::function<void(std::map<int64_t, std::string>)> f) {
if(!f) return;
// only works without multithreading!
// with multithreading onDone could be processed before the last update -> segfault
auto map = new std::map<int64_t, std::string>();
requestMessages(chatid, 0, [this, map, f](int64_t chat) {
f(*map);
delete map;
}, [this, map](objptr<td_api::message> 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<td_api::downloadFile>(file_id, /* prio */ 15, 0, 0, false), HANDLER_NULL);
}
void TGClient::sendTextMessageToSelf(const std::string& text) {
auto sendmsg = td::make_tl_object<td_api::sendMessage>();
sendmsg->chat_id_ = me;
sendmsg->message_thread_id_ = 0;
sendmsg->reply_to_ = nullptr;
sendmsg->options_ = nullptr;
sendmsg->reply_markup_ = nullptr;
std::vector<objptr<td_api::textEntity>> 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<void(objptr<td_api::message>)> f) {
send_wrappedquery<td_api::message>(td::make_tl_object<td_api::getMessage>(chatid, messageid), f);
}
void TGClient::setDraft(int64_t chat, const std::string& text, int64_t replyto, int64_t messageThread) {
auto itext = createMessageInputText(text);
objptr<td_api::draftMessage> dmsg = td_api::make_object<td_api::draftMessage>(replyto, time(nullptr), std::move(itext));
send_staticquery(td_api::make_object<td_api::setChatDraftMessage>(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<td_api::draftMessage> dmsg;
send_staticquery(td_api::make_object<td_api::setChatDraftMessage>(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<int64_t, CachedUser>::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<int64_t, CachedUser>::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<void(int64_t)> onDone, std::function<void(td_api::object_ptr<td_api::message>)> forMessage) {
Log::debug << "requestMessages " << chatid << " from: " << from_message_id;
send_wrappedquery<td_api::messages>(td_api::make_object<td_api::getChatHistory>(chatid, from_message_id, 0, 100, false), [this, chatid, onDone, forMessage](objptr<td_api::messages> m) {
//request next chunk
td_api::array<td_api::object_ptr<td_api::message>>& 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<int64_t>::max();
for(td_api::array<td_api::object_ptr<td_api::message>>::const_reverse_iterator it = arr.rbegin(); it != arr.rend(); ++it) {
const td_api::object_ptr<td_api::message>& 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<int64_t>::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<td_api::OptionValue> val) {
client_manager_->execute(td_api::make_object<td_api::setOption>(name, std::move(val)));
}
void TGClient::setOptions() {
setOption("disable_persistent_network_statistics", td_api::make_object<td_api::optionValueBoolean>(true));
setOption("disable_sent_scheduled_message_notifications", td_api::make_object<td_api::optionValueBoolean>(true));
setOption("disable_time_adjustment_protection", td_api::make_object<td_api::optionValueBoolean>(true));
setOption("disable_top_chats", td_api::make_object<td_api::optionValueBoolean>(true));
setOption("ignore_background_updates", td_api::make_object<td_api::optionValueBoolean>(false));
setOption("ignore_inline_thumbnails", td_api::make_object<td_api::optionValueBoolean>(true));
setOption("ignore_platform_restrictions", td_api::make_object<td_api::optionValueBoolean>(true));
}
void TGClient::loadInit() {
send_wrappedquery<td_api::user>(td_api::make_object<td_api::getMe>(), [this](objptr<td_api::user> meu) {
CachedUser cachedme;
convertUser(*meu, cachedme);
me = cachedme.tgid;
addUser(cachedme);
//load chat list
td_api::object_ptr<td_api::ChatList> mainlist = td_api::make_object<td_api::chatListMain>();
send_staticquery(td_api::make_object<td_api::getChats>(std::move(mainlist), std::numeric_limits<int32_t>::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<td_api::Function> f, std::function<void(Object)> 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<td_api::Function> f, uint64_t handlerid) {
assert(handlerid < STATICHANDLERCOUNT);
client_manager_->send(client_id_, handlerid, std::move(f));
}
template<typename T>
void TGClient::send_wrappedquery(td_api::object_ptr<td_api::Function> f, std::function<void(td::tl_object_ptr<T>)> handler, std::function<void()> onError, bool printError) {
send_query(std::move(f), [handler, onError, printError](Object o) {
if(catchErrors<T>(o, onError, printError) && handler) {
auto casted = td::move_tl_object_as<T>(o);
handler(std::move(casted));
}
});
}
template<typename T>
bool TGClient::catchErrors(const Object& o, std::function<void()> onError, bool printError) {
if(!o) return false;
if(o->get_id() == td_api::error::ID) {
if(printError) {
const objptr<td_api::error>& err = (const objptr<td_api::error>&) 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<typename RequestType, typename ResponseType, typename... Args>
void TGClient::send_inplace(Args... args, std::function<void(td::tl_object_ptr<ResponseType>)> handler, bool printError) {
send_wrappedquery<ResponseType>(td_api::make_object<RequestType>(std::forward<Args>(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<Placeholder>(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<td_api::Object> 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<td_api::draftMessage>& draftmsg = draft.draft_message_;
std::string text;
if(draftmsg) {
const objptr<td_api::InputMessageContent>& 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<int64_t>& 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::message>(td_api::make_object<td_api::getMessage>(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<int32_t, std::string> 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<td_api::checkAuthenticationCode>(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<td_api::registerUser>(first_name, last_name),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitPassword&) {
send_query(td_api::make_object<td_api::checkAuthenticationPassword>(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<td_api::setAuthenticationPhoneNumber>(Config::config.phoneNumber, nullptr),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitEmailAddress&) {
send_query(td_api::make_object<td_api::setAuthenticationEmailAddress>(""),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitEmailCode&) {
send_query(td_api::make_object<td_api::checkAuthenticationCode>(""),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitTdlibParameters&) {
auto parameters = td_api::make_object<td_api::setTdlibParameters>();
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<td_api::error>(object);
std::cout << "Error: " << to_string(error) << std::flush;
on_authorization_state_update();
}
}
std::uint64_t TGClient::next_query_id() {
return ++current_query_id_;
}

110
src/tgtui.cpp Normal file
View File

@ -0,0 +1,110 @@
#include "tgtui.h"
#include <Log.h>
#include <curses.h>
#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<SlimChat>& 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<td_api::chat> chat) {
chats.push_back({chat->id_, chat->title_});
}

270
src/typeconverter.cpp Normal file
View File

@ -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<const td_api::messageSenderUser&>(s).user_id_;
}
if(s.get_id() == td_api::messageSenderChat::ID) {
return static_cast<const td_api::messageSenderChat&>(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<ReplyMarkup>(markup);
}
}
#define CHECKCAST(TYPE, MTYPE, ACCESS) case td_api::TYPE::ID: \
textout = static_cast<const td_api::TYPE&>(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<td_api::object_ptr<td_api::inlineKeyboardButton>>& 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<td_api::message>& 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<td_api::TYPE&>(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<td_api::messageText&>(cont).text_;
}
//default
return nullptr;
}
#undef CHECKCAST

49
src/userstatus.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "userstatus.h"
#include <td/telegram/td_api.h>
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];
}

13
src/view.cpp Normal file
View File

@ -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;
}

61
src/viewchatlist.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "viewchatlist.h"
#include <curses.h>
#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;
}

70
tests/main.cpp Normal file
View File

@ -0,0 +1,70 @@
#include "test.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <string>
#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<std::chrono::high_resolution_clock> 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<int>(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<std::chrono::high_resolution_clock> 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<std::chrono::high_resolution_clock> testEnd = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double, std::milli> 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<std::chrono::high_resolution_clock> end = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double, std::milli> t = end - start;
std::cout << "Testing took: " << t.count() << "ms" << std::endl;
return failcount > 0;
}

12
tests/sampletest.cpp Normal file
View File

@ -0,0 +1,12 @@
#include "test.h"
#include <stdexcept>
#include <Log.h>
// tests are executed top to bottom
TEST(ABC) {
CMPASSERT(1, true);
} TESTEND

43
tests/test.h Normal file
View File

@ -0,0 +1,43 @@
#define TESTFAILED 0
#define TESTGOOD 1
#define TESTSKIPPED -1
#include <iostream>
#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;

1
thirdparty/Log vendored Submodule

@ -0,0 +1 @@
Subproject commit c5274a56b8a306fd184e80bd023824e623c56c27

1
thirdparty/td vendored Submodule

@ -0,0 +1 @@
Subproject commit 00258ccb4cef19e4d6b4392082c68755887d9c2f