Add more features
This commit is contained in:
parent
859e1b2835
commit
2a4c54ad53
4 changed files with 158 additions and 25 deletions
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# brainlet-client
|
||||||
|
The better client for Brainlet
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
- Linux (`linux`)
|
||||||
|
- gtkmm3 (`gtkmm3`)
|
||||||
|
- make (`make`)
|
||||||
|
- ccache (`ccache`)
|
||||||
|
- clang (`clang`)
|
||||||
|
- libcurl (`curl`)
|
||||||
|
- websocketpp (`websocketpp`)
|
||||||
|
- libjsoncpp (`jsoncpp`)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
- Chatting
|
||||||
|
- Markup using the [Pango markup language](https://developer.gnome.org/pygtk/stable/pango-markup-language.html)
|
57
brainlet.h
57
brainlet.h
|
@ -56,7 +56,7 @@ namespace Brainlet {
|
||||||
std::string color; ///< Color represented in #rrggbb
|
std::string color; ///< Color represented in #rrggbb
|
||||||
} User;
|
} User;
|
||||||
|
|
||||||
class Client : private User {
|
class Client : public User {
|
||||||
private:
|
private:
|
||||||
enum State {
|
enum State {
|
||||||
NOT_CONNECTED,
|
NOT_CONNECTED,
|
||||||
|
@ -73,6 +73,7 @@ namespace Brainlet {
|
||||||
State state = NOT_CONNECTED;
|
State state = NOT_CONNECTED;
|
||||||
|
|
||||||
static void onReceive(Client* client, std::string buffer);
|
static void onReceive(Client* client, std::string buffer);
|
||||||
|
void encode(std::string& string); ///< Encodes a string to a JSON value
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::function<void(Channel)> onNewChannel = nullptr;
|
std::function<void(Channel)> onNewChannel = nullptr;
|
||||||
|
@ -81,8 +82,8 @@ namespace Brainlet {
|
||||||
Client(const std::string& domain);
|
Client(const std::string& domain);
|
||||||
|
|
||||||
void processNextEvent();
|
void processNextEvent();
|
||||||
void login(const std::string& name, const std::string& password);
|
void login(std::string name, std::string password);
|
||||||
void sendMessage(const std::string& message, const std::string& channelId);
|
void sendMessage(std::string message, std::string channelId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +104,7 @@ namespace Brainlet {
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdlib> // std;:strtol
|
#include <cstdlib> // std;:strtol
|
||||||
#include <algorithm> // std::for_each
|
#include <algorithm> // std::for_each
|
||||||
|
#include <utility> // std::move
|
||||||
#include <string.h> // strerror
|
#include <string.h> // strerror
|
||||||
|
|
||||||
#include <json/json.h>
|
#include <json/json.h>
|
||||||
|
@ -110,6 +112,25 @@ namespace Brainlet {
|
||||||
namespace Brainlet {
|
namespace Brainlet {
|
||||||
Client::Client(const std::string& domain) : domain(domain) {}
|
Client::Client(const std::string& domain) : domain(domain) {}
|
||||||
|
|
||||||
|
void Client::encode(std::string& string) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(string.size());
|
||||||
|
|
||||||
|
std::for_each(string.begin(), string.end(), [&](const char character) {
|
||||||
|
switch (character) {
|
||||||
|
case '"': result.append("\\\""); break;
|
||||||
|
case '\\': result.append("\\\\"); break;
|
||||||
|
case '\b': result.append("\\b"); break;
|
||||||
|
case '\f': result.append("\\f"); break;
|
||||||
|
case '\n': result.append("\\n"); break;
|
||||||
|
case '\r': result.append("\\r"); break;
|
||||||
|
case '\t': result.append("\\t"); break;
|
||||||
|
default: result.push_back(character); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
string = std::move(result);
|
||||||
|
}
|
||||||
void Client::processNextEvent() {
|
void Client::processNextEvent() {
|
||||||
socket->run_until(
|
socket->run_until(
|
||||||
// Stop in 250 microseconds
|
// Stop in 250 microseconds
|
||||||
|
@ -117,10 +138,30 @@ namespace Brainlet {
|
||||||
std::chrono::microseconds(250)
|
std::chrono::microseconds(250)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
void Client::login(const std::string& name, const std::string& password) {
|
void Client::login(std::string name, std::string password) {
|
||||||
|
{
|
||||||
|
// Escape the name and password
|
||||||
|
auto escape = [](std::string& string) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(string.size());
|
||||||
|
|
||||||
|
std::for_each(string.begin(), string.end(), [&](const char character) {
|
||||||
|
switch (character) {
|
||||||
|
case '%': result.append("%25"); break;
|
||||||
|
case '&': result.append("%26"); break;
|
||||||
|
default: result.push_back(character); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
string = std::move(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
escape(name);
|
||||||
|
escape(password);
|
||||||
|
}
|
||||||
|
|
||||||
Http::Request request("http://" + domain + "/api/v1/users/token/create");
|
Http::Request request("http://" + domain + "/api/v1/users/token/create");
|
||||||
|
|
||||||
// TODO: Escape &
|
|
||||||
const std::string postFields = "username="+name+"&password="+password+"&alsoSetCookie=true";
|
const std::string postFields = "username="+name+"&password="+password+"&alsoSetCookie=true";
|
||||||
request.setPostFields(postFields);
|
request.setPostFields(postFields);
|
||||||
request.perform();
|
request.perform();
|
||||||
|
@ -216,6 +257,7 @@ namespace Brainlet {
|
||||||
};
|
};
|
||||||
if (client->onNewMessage)
|
if (client->onNewMessage)
|
||||||
client->onNewMessage(message, channel);
|
client->onNewMessage(message, channel);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n";
|
std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n";
|
||||||
|
@ -225,9 +267,10 @@ namespace Brainlet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void Client::sendMessage(const std::string& message, const std::string& channelId) {
|
void Client::sendMessage(std::string message, std::string channelId) {
|
||||||
|
encode(message);
|
||||||
|
|
||||||
std::ostringstream bufferToSend;
|
std::ostringstream bufferToSend;
|
||||||
// TODO: Escape " from message
|
|
||||||
bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})";
|
bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})";
|
||||||
socket->send(bufferToSend.str());
|
socket->send(bufferToSend.str());
|
||||||
}
|
}
|
||||||
|
|
74
design.glade
74
design.glade
|
@ -23,6 +23,13 @@ SOFTWARE.
|
||||||
<object class="GtkPaned" id="Chat">
|
<object class="GtkPaned" id="Chat">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
|
<child>
|
||||||
|
<!-- n-columns=2 n-rows=2 -->
|
||||||
|
<object class="GtkGrid">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
<object class="GtkScrolledWindow">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -31,6 +38,7 @@ SOFTWARE.
|
||||||
<property name="margin-end">5</property>
|
<property name="margin-end">5</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<property name="vexpand">True</property>
|
<property name="vexpand">True</property>
|
||||||
<property name="shadow-type">in</property>
|
<property name="shadow-type">in</property>
|
||||||
<child>
|
<child>
|
||||||
|
@ -46,6 +54,44 @@ SOFTWARE.
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
<property name="width">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="UsernameLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="margin-start">5</property>
|
||||||
|
<property name="margin-end">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="label" translatable="yes">[Username goes here]</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="SettingsButton">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="events">GDK_BUTTON_PRESS_MASK</property>
|
||||||
|
<property name="margin-start">5</property>
|
||||||
|
<property name="margin-end">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="icon-name">applications-system-symbolic.symbolic</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="resize">True</property>
|
<property name="resize">True</property>
|
||||||
<property name="shrink">True</property>
|
<property name="shrink">True</property>
|
||||||
|
@ -56,6 +102,7 @@ SOFTWARE.
|
||||||
<object class="GtkGrid">
|
<object class="GtkGrid">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<property name="vexpand">True</property>
|
<property name="vexpand">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
<object class="GtkScrolledWindow">
|
||||||
|
@ -76,6 +123,7 @@ SOFTWARE.
|
||||||
<object class="GtkListBox" id="Messages">
|
<object class="GtkListBox" id="Messages">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
|
<property name="selection-mode">none</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkListBoxRow">
|
<object class="GtkListBoxRow">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -86,6 +134,7 @@ SOFTWARE.
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="label" translatable="yes">No channel has been opened.
|
<property name="label" translatable="yes">No channel has been opened.
|
||||||
Select a channel on the box to the left.</property>
|
Select a channel on the box to the left.</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -106,12 +155,10 @@ Select a channel on the box to the left.</property>
|
||||||
<object class="GtkEntry" id="Message">
|
<object class="GtkEntry" id="Message">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="margin-start">5</property>
|
<property name="max-length">2000</property>
|
||||||
<property name="margin-end">5</property>
|
<property name="placeholder-text" translatable="yes">Message (2000 character max)</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="input-hints">GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_EMOJI | GTK_INPUT_HINT_NONE</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="show-emoji-icon">True</property>
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<property name="placeholder-text" translatable="yes">Message</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left-attach">0</property>
|
<property name="left-attach">0</property>
|
||||||
|
@ -132,6 +179,21 @@ Select a channel on the box to the left.</property>
|
||||||
<property name="buttons">ok</property>
|
<property name="buttons">ok</property>
|
||||||
<property name="text" translatable="yes">An error has occured</property>
|
<property name="text" translatable="yes">An error has occured</property>
|
||||||
<property name="secondary-text" translatable="yes">[Error message goes here]</property>
|
<property name="secondary-text" translatable="yes">[Error message goes here]</property>
|
||||||
|
<child internal-child="vbox">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child internal-child="action_area">
|
||||||
|
<object class="GtkButtonBox">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<!-- n-columns=3 n-rows=5 -->
|
<!-- n-columns=3 n-rows=5 -->
|
||||||
<object class="GtkGrid" id="Login">
|
<object class="GtkGrid" id="Login">
|
||||||
|
|
16
main.cpp
16
main.cpp
|
@ -45,6 +45,7 @@ Gtk::Widget* chatInterface;
|
||||||
Gtk::ListBox* channelList;
|
Gtk::ListBox* channelList;
|
||||||
Gtk::ListBox* messageList;
|
Gtk::ListBox* messageList;
|
||||||
Gtk::Entry* messageEntry;
|
Gtk::Entry* messageEntry;
|
||||||
|
Gtk::Label* usernameLabel;
|
||||||
|
|
||||||
gboolean update(gpointer) {
|
gboolean update(gpointer) {
|
||||||
client->processNextEvent();
|
client->processNextEvent();
|
||||||
|
@ -56,6 +57,7 @@ void addChannel(const Brainlet::Channel channel) {
|
||||||
auto name = Gtk::make_managed<Gtk::Label>();
|
auto name = Gtk::make_managed<Gtk::Label>();
|
||||||
|
|
||||||
name->set_halign(Gtk::ALIGN_START);
|
name->set_halign(Gtk::ALIGN_START);
|
||||||
|
name->set_line_wrap(true);
|
||||||
name->set_text(channel.name);
|
name->set_text(channel.name);
|
||||||
|
|
||||||
row->add(*name);
|
row->add(*name);
|
||||||
|
@ -76,13 +78,18 @@ void addMessage(const Brainlet::Message message) {
|
||||||
auto text = Gtk::make_managed<Gtk::Label>();
|
auto text = Gtk::make_managed<Gtk::Label>();
|
||||||
|
|
||||||
text->set_halign(Gtk::ALIGN_START);
|
text->set_halign(Gtk::ALIGN_START);
|
||||||
|
text->set_line_wrap(true);
|
||||||
|
|
||||||
std::string messageFormatted = message.authorId + ": " + message.message;
|
std::string messageFormatted = message.authorId + ": " + message.message;
|
||||||
text->set_text(messageFormatted.c_str());
|
text->set_markup(messageFormatted.c_str());
|
||||||
|
|
||||||
row->add(*text);
|
row->add(*text);
|
||||||
messageList->add(*row);
|
messageList->add(*row);
|
||||||
|
|
||||||
|
std::string mention = "@" + client->id;
|
||||||
|
if (message.message.find(mention) != std::string::npos)
|
||||||
|
messageList->select_row(*row);
|
||||||
|
|
||||||
row->show_all_children();
|
row->show_all_children();
|
||||||
row->show();
|
row->show();
|
||||||
}
|
}
|
||||||
|
@ -105,14 +112,19 @@ int main(int argc, char* argv[]) {
|
||||||
builder->get_widget("Channels", channelList);
|
builder->get_widget("Channels", channelList);
|
||||||
builder->get_widget("Messages", messageList);
|
builder->get_widget("Messages", messageList);
|
||||||
builder->get_widget("Message", messageEntry);
|
builder->get_widget("Message", messageEntry);
|
||||||
|
builder->get_widget("UsernameLabel", usernameLabel);
|
||||||
|
|
||||||
auto login = [&]() {
|
auto login = [&]() {
|
||||||
if (client.get() != nullptr) return;
|
if (client.get() != nullptr) return;
|
||||||
|
|
||||||
|
const std::string username = usernameEntry->get_text();
|
||||||
|
const std::string password = passwordEntry->get_text();
|
||||||
|
usernameLabel->set_text(username.c_str());
|
||||||
window->remove();
|
window->remove();
|
||||||
window->add(*chatInterface);
|
window->add(*chatInterface);
|
||||||
|
|
||||||
client.reset(new Brainlet::Client(domainEntry->get_text()));
|
client.reset(new Brainlet::Client(domainEntry->get_text()));
|
||||||
client->login(usernameEntry->get_text(), passwordEntry->get_text());
|
client->login(username, password);
|
||||||
|
|
||||||
client->onNewChannel = addChannel;
|
client->onNewChannel = addChannel;
|
||||||
client->onNewMessage = [&](const Brainlet::Message message, const Brainlet::Channel channel) {
|
client->onNewMessage = [&](const Brainlet::Message message, const Brainlet::Channel channel) {
|
||||||
|
|
Loading…
Reference in a new issue