From 859e1b28357637a8e3229bcd888675ea2cbd3f77 Mon Sep 17 00:00:00 2001 From: hiimgoodpack Date: Thu, 25 Mar 2021 19:59:07 -0400 Subject: [PATCH] Add user interface --- Makefile | 10 +- brainlet.h | 236 +++++++++++++++++++++++++++++++++++++++++ design.glade | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++ http.h | 7 +- main.cpp | 163 ++++++++++++++++++++++------- websocket.h | 18 ++-- 6 files changed, 668 insertions(+), 54 deletions(-) create mode 100644 brainlet.h create mode 100644 design.glade diff --git a/Makefile b/Makefile index 84de24b..b843dc1 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -CPPFLAGS=-lstdc++ -lm -lpthread -lcurl -std=c++11 -ggdb -Wall +CPPFLAGS=-lstdc++ -lm -lpthread -lcurl -ljsoncpp -std=c++11 -ggdb -Wall -march=native CXX=ccache clang all: main @@ -24,8 +24,10 @@ websocket.o: websocket.h $(CXX) $(CPPFLAGS) -x c++ -DWEBSOCKET_IMPLEMENTATION -c -o websocket.o websocket.h http.o: http.h $(CXX) $(CPPFLAGS) -x c++ -DHTTP_IMPLEMENTATION -c -o http.o http.h +brainlet.o: brainlet.h + $(CXX) $(CPPFLAGS) -x c++ -DBRAINLET_IMPLEMENTATION -c -o brainlet.o brainlet.h main.o: main.cpp - $(CXX) $(CPPFLAGS) -c -o main.o main.cpp + $(CXX) $(CPPFLAGS) -I/usr/include/gtkmm-3.0 -I/usr/lib/gtkmm-3.0/include -I/usr/include/giomm-2.4 -I/usr/lib/giomm-2.4/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/glibmm-2.4 -I/usr/lib/glibmm-2.4/include -I/usr/include/sigc++-2.0 -I/usr/lib/sigc++-2.0/include -I/usr/include/gtk-3.0 -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/lzo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/gio-unix-2.0 -I/usr/include/cloudproviders -I/usr/include/atk-1.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/at-spi-2.0 -I/usr/include/cairomm-1.0 -I/usr/lib/cairomm-1.0/include -I/usr/include/pangomm-1.4 -I/usr/lib/pangomm-1.4/include -I/usr/include/atkmm-1.6 -I/usr/lib/atkmm-1.6/include -I/usr/include/gtk-3.0/unix-print -I/usr/include/gdkmm-3.0 -I/usr/lib/gdkmm-3.0/include -c -o main.o main.cpp -main: websocket.o http.o main.o - $(CXX) $(CPPFLAGS) websocket.o http.o main.o +main: websocket.o http.o brainlet.o main.o + $(CXX) $(CPPFLAGS) -lgtkmm-3.0 -latkmm-1.6 -lgdkmm-3.0 -lgiomm-2.4 -lgtk-3 -lgdk-3 -lz -latk-1.0 -lcairo-gobject -lgio-2.0 -lpangomm-1.4 -lglibmm-2.4 -lcairomm-1.0 -lsigc-2.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lcairo -lgdk_pixbuf-2.0 -lgobject-2.0 -lglib-2.0 websocket.o http.o brainlet.o main.o diff --git a/brainlet.h b/brainlet.h new file mode 100644 index 0000000..c7a441a --- /dev/null +++ b/brainlet.h @@ -0,0 +1,236 @@ +// Copyright (c) 2021 hiimgoodpack +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef BRAINLET_H +#define BRAINLET_H + +#include +#include // std::unique_ptr +#include +#include +#include "websocket.h" + +namespace Brainlet { + enum Opcode { + HELLO = 0, + YOO = 1, + YOO_ACK = 2, + ACTION_CREATE_MESSAGE = 3, + EVENT_NEW_MESSAGE = 4, + ACTION_UPDATE_STATUS = 5, + EVENT_CHANNEL_MEMBERS = 6, + EVENT_CHANNEL_MEMBER_UPDATE = 7, + }; + + const std::size_t minChannelNameLength = 3; + const std::size_t maxChannelNameLength = 32; + const std::size_t idLength = 24; + + typedef struct { + std::string id; + std::string name; + } Channel; + + typedef struct { + std::string authorId; + std::string message; + } Message; + + typedef struct { + std::string id; + std::string name; + std::string color; ///< Color represented in #rrggbb + } User; + + class Client : private User { + private: + enum State { + NOT_CONNECTED, + WAIT_HELLO, + WAIT_ACK, + READY + }; + + std::string domain; + std::string token; + + std::unique_ptr socket = nullptr; + + State state = NOT_CONNECTED; + + static void onReceive(Client* client, std::string buffer); + + public: + std::function onNewChannel = nullptr; + std::function onNewMessage = nullptr; + + Client(const std::string& domain); + + void processNextEvent(); + void login(const std::string& name, const std::string& password); + void sendMessage(const std::string& message, const std::string& channelId); + }; +} + +#endif + +#if defined(BRAINLET_IMPLEMENTATION) && !defined(BRAINLET_IMPLEMENTED) + +#include "http.h" + +#include // std::strncmp +#include +#include +#include // std::istringstream, std::ostringstream +#include // std::snprintf +#include +#include +#include +#include +#include // std;:strtol +#include // std::for_each +#include // strerror + +#include + +namespace Brainlet { + Client::Client(const std::string& domain) : domain(domain) {} + + void Client::processNextEvent() { + socket->run_until( + // Stop in 250 microseconds + std::chrono::system_clock::now() + + std::chrono::microseconds(250) + ); + } + void Client::login(const std::string& name, const std::string& password) { + Http::Request request("http://" + domain + "/api/v1/users/token/create"); + + // TODO: Escape & + const std::string postFields = "username="+name+"&password="+password+"&alsoSetCookie=true"; + request.setPostFields(postFields); + request.perform(); + + Json::Value value; + std::istringstream(request.buffer) >> value; + + bool didError = value.get("error", true).asBool(); + if (didError) { + // TODO: Use exception + std::cerr << "Error while logging in: " << value.get("message", "Server did not specify error").asString() << "\n"; + return; + } + + token = value.get("token", "Unknown token").asString(); + id = value["user"].get("id", "0").asString(); + color = value["user"].get("color", "#ffffff").asString(); + + socket.reset(new WebSocket("ws://" + domain + "/gateway")); + state = WAIT_HELLO; + socket->onReceive = std::bind(onReceive, this, std::placeholders::_1); + } + void Client::onReceive(Client* client, std::string buffer) { + Json::Value data; + Opcode opcode; + { + char* characterAfterType = nullptr; + errno = 0; + opcode = (Opcode)std::strtol(buffer.c_str(), &characterAfterType, 10); + if (errno != 0) { + // TODO: Use exception + std::cerr << "Failed to get opcode: " << strerror(errno) << "\n"; + return; + } + + // Skip opcode number and ending @ + unsigned int charactersToSkip = characterAfterType - buffer.c_str() + 1; + std::istringstream stream(buffer); + for (unsigned int i = 0; i < charactersToSkip; i++) { + char _; stream >> _; + } + + stream >> data; + } + switch (client->state) { + case NOT_CONNECTED: throw std::logic_error("Buffer was received while client is not connected"); + case WAIT_HELLO: { + if (opcode != HELLO) { + std::cerr << "Failed authentication: Expected HELLO packet, received: " << buffer << "\n"; + client->socket->close(); + return; + } + + client->state = WAIT_ACK; + + char bufferToSend[256]; + std::snprintf(bufferToSend, sizeof(bufferToSend), R"(%i@{"token":"%s"})", + YOO, client->token.c_str()); + client->socket->send(bufferToSend); + break; + } + case WAIT_ACK: { + if (opcode != YOO_ACK) { + std::cerr << "Failed authentication: Expected YOO_ACK packet, received: " << buffer << "\n"; + client->socket->close(); + return; + } + + client->state = READY; + + const Json::Value channels = data["channels"]; + for (Json::ArrayIndex i = 0; i < channels.size(); ++i) { + const Json::Value channelValue = channels[i]; + const Channel channelObject = { + .id = channelValue["_id"].asString(), + .name = channelValue["title"].asString() + }; + + if (client->onNewChannel) + client->onNewChannel(channelObject); + } + } + case READY: { + switch (opcode) { + case EVENT_NEW_MESSAGE: { + Channel channel = { + .id = data["channel"]["_id"].asString(), + .name = data["channel"]["title"].asString() + }; + Message message = { + .authorId = data["author"]["_id"].asString(), + .message = data["content"].asString() + }; + if (client->onNewMessage) + client->onNewMessage(message, channel); + } + default: { + std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n"; + break; + } + } + } + } + } + void Client::sendMessage(const std::string& message, const std::string& channelId) { + std::ostringstream bufferToSend; + // TODO: Escape " from message + bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})"; + socket->send(bufferToSend.str()); + } +} + +#endif diff --git a/design.glade b/design.glade new file mode 100644 index 0000000..11781b6 --- /dev/null +++ b/design.glade @@ -0,0 +1,288 @@ + + + + + + + True + True + + + True + True + 5 + 5 + 5 + 5 + True + in + + + True + False + + + True + False + + + + + + + True + True + + + + + + True + False + True + + + True + True + 5 + 5 + 5 + 5 + True + True + in + + + True + False + + + True + False + + + True + True + + + True + False + No channel has been opened. +Select a channel on the box to the left. + + + + + + + + + + + 0 + 0 + 3 + 2 + + + + + True + True + 5 + 5 + 5 + 5 + True + Message + + + 0 + 2 + 3 + + + + + True + True + + + + + False + dialog + ok + An error has occured + [Error message goes here] + + + + True + False + True + True + + + True + False + 5 + 5 + 5 + 5 + Log into Brainlet + + + 0 + 0 + 3 + + + + + True + False + 5 + 5 + 5 + 5 + Domain: + right + + + 0 + 1 + + + + + True + True + 5 + 5 + 5 + 5 + example.com + url + + + 1 + 1 + 2 + + + + + True + False + 5 + 5 + 5 + 5 + Username: + right + + + 0 + 2 + + + + + True + True + 5 + 5 + 5 + 5 + 32 + 32 + False + name + GTK_INPUT_HINT_LOWERCASE | GTK_INPUT_HINT_UPPERCASE_CHARS | GTK_INPUT_HINT_NONE + + + 1 + 2 + 2 + + + + + True + False + 5 + 5 + 5 + 5 + Password: + right + + + 0 + 3 + + + + + True + True + 5 + 5 + 5 + 5 + True + 128 + False + 32 + password + + + 1 + 3 + 2 + + + + + Log in + True + True + True + 5 + 5 + 5 + 5 + + + 0 + 4 + 3 + + + + + False + Brainlet + + + + + diff --git a/http.h b/http.h index a565953..af76f12 100644 --- a/http.h +++ b/http.h @@ -32,6 +32,7 @@ namespace Http { class Request { private: CURL* handle; + std::string url; std::string postFields; static std::size_t write(void* buffer, std::size_t blocks, std::size_t blockSize, std::string* result); @@ -40,7 +41,7 @@ namespace Http { std::string buffer; long int status; - Request(const char* const url); + Request(const std::string& url); void setPostFields(std::string fields); void perform(); ~Request(); @@ -53,10 +54,10 @@ namespace Http { #define HTTP_IMPLEMENTED namespace Http { - Request::Request(const char* const url) { + Request::Request(const std::string& newUrl) : url(newUrl) { handle = curl_easy_init(); - curl_easy_setopt(handle, CURLOPT_URL, url); + curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status); curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write); diff --git a/main.cpp b/main.cpp index 6efb3ac..14df3a4 100644 --- a/main.cpp +++ b/main.cpp @@ -17,50 +17,135 @@ #include "http.h" #include "websocket.h" +#include "brainlet.h" +#include // std::strcpy +#include // std::unique_ptr +#include // std::for_each +#include +#include +#include #include -#include // std::strncmp -#include -#include -#include -enum State { - WAIT_HELLO, - WAIT_ACK, - READY -}; +std::unique_ptr client = nullptr; + +std::unordered_map channelRowToId; +std::unordered_map> messages; +std::string currentChannelId; + +Gtk::Window* window; + +Gtk::Widget* loginInterface; +Gtk::Entry* domainEntry; +Gtk::Entry* usernameEntry; +Gtk::Entry* passwordEntry; +Gtk::Button* loginButton; + +Gtk::Widget* chatInterface; +Gtk::ListBox* channelList; +Gtk::ListBox* messageList; +Gtk::Entry* messageEntry; + +gboolean update(gpointer) { + client->processNextEvent(); + return G_SOURCE_CONTINUE; +} + +void addChannel(const Brainlet::Channel channel) { + auto row = Gtk::make_managed(); + auto name = Gtk::make_managed(); + + name->set_halign(Gtk::ALIGN_START); + name->set_text(channel.name); + + row->add(*name); + channelList->add(*row); + + row->show_all_children(); + row->show(); + + channelRowToId.emplace(row, channel.id); + messages.emplace( + std::piecewise_construct, + std::forward_as_tuple(channel.id), + std::forward_as_tuple() + ); +} +void addMessage(const Brainlet::Message message) { + auto row = Gtk::make_managed(); + auto text = Gtk::make_managed(); + + text->set_halign(Gtk::ALIGN_START); + + std::string messageFormatted = message.authorId + ": " + message.message; + text->set_text(messageFormatted.c_str()); + + row->add(*text); + messageList->add(*row); + + row->show_all_children(); + row->show(); +} + int main(int argc, char* argv[]) { curl_global_init(CURL_GLOBAL_ALL); - Http::Request tokenRequest("http://localhost:3005/api/v1/users/token/create"); - tokenRequest.setPostFields("username=TestUser&password=12345678&alsoSetCookie=true}"); - tokenRequest.perform(); - std::cout << tokenRequest.status << " " << tokenRequest.buffer; - WebSocket socket("ws://localhost:3005/gateway"); - State state = WAIT_HELLO; - socket.onReceive = [&](std::string message) { - std::cout << "Received: " << message << "\n"; - switch (state) { - case WAIT_HELLO: { - if (std::strncmp(message.c_str(), "0@", 2) == 0) { - state = WAIT_ACK; - socket.send("1@{\"token\":\"my totally real token\""); - } else { - std::cerr << "Failed authentication: Expected packet type 0 (HELLO), received buffer: " << message << "\n"; - socket.close(); - } - break; - } - case WAIT_ACK: { - if (std::strncmp(message.c_str(), "2@", 2) == 0) { - state = READY; - } else { - std::cerr << "Failed authentication: Expected packet type 2 (YOO_ACK), received buffer: " << message << "\n"; - socket.close(); - } - } - case READY: break; - } + Glib::RefPtr app = Gtk::Application::create("org.hiimgoodpack.brainlet_client"); + Glib::RefPtr builder = Gtk::Builder::create_from_file("./design.glade"); + + builder->get_widget("Window", window); + + builder->get_widget("Login", loginInterface); + builder->get_widget("Domain", domainEntry); + builder->get_widget("Username", usernameEntry); + builder->get_widget("Password", passwordEntry); + builder->get_widget("LoginButton", loginButton); + + builder->get_widget("Chat", chatInterface); + builder->get_widget("Channels", channelList); + builder->get_widget("Messages", messageList); + builder->get_widget("Message", messageEntry); + + auto login = [&]() { + if (client.get() != nullptr) return; + window->remove(); + window->add(*chatInterface); + + client.reset(new Brainlet::Client(domainEntry->get_text())); + client->login(usernameEntry->get_text(), passwordEntry->get_text()); + + client->onNewChannel = addChannel; + client->onNewMessage = [&](const Brainlet::Message message, const Brainlet::Channel channel) { + messages.at(channel.id).push_back(message); + if (channel.id == currentChannelId) + addMessage(message); + }; + + g_timeout_add((1.0f/5.0f)*1000.0f, update, nullptr); }; - socket.run(); + passwordEntry->signal_activate().connect(login); + loginButton->signal_clicked().connect(login); + + channelList->signal_row_activated().connect([&](Gtk::ListBoxRow* row) { + // Clear messages in messageList + messageList->foreach(std::bind(std::mem_fn(&Gtk::ListBox::remove), messageList, std::placeholders::_1)); + + // Add messages in the channel selected + currentChannelId = channelRowToId.at(row); + + const std::vector& channelMessages = messages.at(currentChannelId); + std::for_each(channelMessages.begin(), channelMessages.end(), addMessage); + }); + + messageEntry->signal_activate().connect([&]() { + // TODO: Show message locally first to avoid latency + // Also show some sort of unread indicator + client->sendMessage(messageEntry->get_text(), currentChannelId); + + messageEntry->set_text(""); + }); + + window->add(*loginInterface); + + app->run(*window); } diff --git a/websocket.h b/websocket.h index 6c3be1e..5ce7a2b 100644 --- a/websocket.h +++ b/websocket.h @@ -32,7 +32,7 @@ class WebSocketError : public std::runtime_error { using runtime_error::runtime_error; }; -class WebSocket { +class WebSocket : public websocketpp::lib::asio::io_service { private: websocketpp::client client; websocketpp::connection_hdl hdl; @@ -42,8 +42,7 @@ class WebSocket { static void onCloseHandler(WebSocket*); public: - WebSocket(const char* const url); - void run(); + WebSocket(const std::string& url); void send(std::string buffer); void close(); @@ -58,9 +57,13 @@ class WebSocket { #define WEBSOCKET_IMPLEMENTED #include +#include -WebSocket::WebSocket(const char* const url) { - client.init_asio(); +WebSocket::WebSocket(const std::string& url) { + client.clear_access_channels(websocketpp::log::alevel::all); + client.clear_error_channels(websocketpp::log::elevel::all); + + client.init_asio(reinterpret_cast(this)); client.set_open_handler(std::bind(onConnectHandler, this, std::placeholders::_1)); client.set_message_handler(std::bind(onReceiveHandler, this, std::placeholders::_2)); @@ -79,6 +82,7 @@ void WebSocket::onConnectHandler(WebSocket* socket, websocketpp::connection_hdl socket->onConnect(); } void WebSocket::onReceiveHandler(WebSocket* socket, websocketpp::config::asio_client::message_type::ptr message) { + std::cerr << "Received: " << message->get_payload() << "\n"; if (socket->onReceive) socket->onReceive(message->get_payload()); } @@ -87,10 +91,8 @@ void WebSocket::onCloseHandler(WebSocket* socket) { socket->onClose(); } -void WebSocket::run() { - client.run(); -} void WebSocket::send(std::string buffer) { + std::cerr << "Sending: " << buffer << "\n"; client.send(hdl, buffer, websocketpp::frame::opcode::text); } void WebSocket::close() {