Add more features

This commit is contained in:
hiimgoodpack 2021-03-26 12:06:08 -04:00
parent 859e1b2835
commit 2a4c54ad53
Signed by: hiimgoodpack
GPG key ID: 4E0E62733C14AE69
4 changed files with 158 additions and 25 deletions

16
README.md Normal file
View 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)

View file

@ -56,7 +56,7 @@ namespace Brainlet {
std::string color; ///< Color represented in #rrggbb
} User;
class Client : private User {
class Client : public User {
private:
enum State {
NOT_CONNECTED,
@ -73,6 +73,7 @@ namespace Brainlet {
State state = NOT_CONNECTED;
static void onReceive(Client* client, std::string buffer);
void encode(std::string& string); ///< Encodes a string to a JSON value
public:
std::function<void(Channel)> onNewChannel = nullptr;
@ -81,8 +82,8 @@ namespace Brainlet {
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);
void login(std::string name, std::string password);
void sendMessage(std::string message, std::string channelId);
};
}
@ -103,6 +104,7 @@ namespace Brainlet {
#include <cassert>
#include <cstdlib> // std;:strtol
#include <algorithm> // std::for_each
#include <utility> // std::move
#include <string.h> // strerror
#include <json/json.h>
@ -110,6 +112,25 @@ namespace Brainlet {
namespace Brainlet {
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() {
socket->run_until(
// Stop in 250 microseconds
@ -117,10 +138,30 @@ namespace Brainlet {
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");
// TODO: Escape &
const std::string postFields = "username="+name+"&password="+password+"&alsoSetCookie=true";
request.setPostFields(postFields);
request.perform();
@ -216,6 +257,7 @@ namespace Brainlet {
};
if (client->onNewMessage)
client->onNewMessage(message, channel);
break;
}
default: {
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;
// TODO: Escape " from message
bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})";
socket->send(bufferToSend.str());
}

View file

@ -24,26 +24,72 @@ SOFTWARE.
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkScrolledWindow">
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">True</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="can-focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkViewport">
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="can-focus">True</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="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox" id="Channels">
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkListBox" id="Channels">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
</child>
</object>
</child>
</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>
@ -56,6 +102,7 @@ SOFTWARE.
<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>
<object class="GtkScrolledWindow">
@ -76,6 +123,7 @@ SOFTWARE.
<object class="GtkListBox" id="Messages">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="selection-mode">none</property>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
@ -86,6 +134,7 @@ SOFTWARE.
<property name="can-focus">False</property>
<property name="label" translatable="yes">No channel has been opened.
Select a channel on the box to the left.</property>
<property name="wrap">True</property>
</object>
</child>
</object>
@ -106,12 +155,10 @@ Select a channel on the box to the left.</property>
<object class="GtkEntry" id="Message">
<property name="visible">True</property>
<property name="can-focus">True</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="hexpand">True</property>
<property name="placeholder-text" translatable="yes">Message</property>
<property name="max-length">2000</property>
<property name="placeholder-text" translatable="yes">Message (2000 character max)</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="show-emoji-icon">True</property>
</object>
<packing>
<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="text" translatable="yes">An error has occured</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>
<!-- n-columns=3 n-rows=5 -->
<object class="GtkGrid" id="Login">

View file

@ -45,6 +45,7 @@ Gtk::Widget* chatInterface;
Gtk::ListBox* channelList;
Gtk::ListBox* messageList;
Gtk::Entry* messageEntry;
Gtk::Label* usernameLabel;
gboolean update(gpointer) {
client->processNextEvent();
@ -56,6 +57,7 @@ void addChannel(const Brainlet::Channel channel) {
auto name = Gtk::make_managed<Gtk::Label>();
name->set_halign(Gtk::ALIGN_START);
name->set_line_wrap(true);
name->set_text(channel.name);
row->add(*name);
@ -76,13 +78,18 @@ void addMessage(const Brainlet::Message message) {
auto text = Gtk::make_managed<Gtk::Label>();
text->set_halign(Gtk::ALIGN_START);
text->set_line_wrap(true);
std::string messageFormatted = message.authorId + ": " + message.message;
text->set_text(messageFormatted.c_str());
text->set_markup(messageFormatted.c_str());
row->add(*text);
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();
}
@ -105,14 +112,19 @@ int main(int argc, char* argv[]) {
builder->get_widget("Channels", channelList);
builder->get_widget("Messages", messageList);
builder->get_widget("Message", messageEntry);
builder->get_widget("UsernameLabel", usernameLabel);
auto login = [&]() {
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->add(*chatInterface);
client.reset(new Brainlet::Client(domainEntry->get_text()));
client->login(usernameEntry->get_text(), passwordEntry->get_text());
client->login(username, password);
client->onNewChannel = addChannel;
client->onNewMessage = [&](const Brainlet::Message message, const Brainlet::Channel channel) {