This repository has been archived on 2021-04-23. You can view files and clone it, but cannot push or open issues or pull requests.
brainlet-client/brainlet.h

310 lines
8.6 KiB
C++

// 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 <string>
#include <memory> // std::unique_ptr
#include <cstdint>
#include <functional>
#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 id;
std::string username;
} MessageAuthor;
typedef struct {
MessageAuthor author;
std::string message;
} Message;
typedef struct {
std::string id;
std::string name;
std::string color; ///< Color represented in #rrggbb
} User;
class Client : public User {
private:
enum State {
NOT_CONNECTED,
WAIT_HELLO,
WAIT_ACK,
READY
};
std::string domain;
std::string token;
std::unique_ptr<WebSocket> socket = nullptr;
State state = NOT_CONNECTED;
static void onReceive(Client* client, std::string buffer);
static void escapeJSON(std::string& string); ///< Escapes a string to a JSON value
static void escapeOutput(std::string& string); ///< Escapes a string to output safely
public:
std::function<void(Channel)> onNewChannel = nullptr;
std::function<void(Message, Channel)> onNewMessage = nullptr;
Client(const std::string& domain);
void processNextEvent();
void login(std::string name, std::string password);
void sendMessage(std::string message, std::string channelId);
};
}
#endif
#if defined(BRAINLET_IMPLEMENTATION) && !defined(BRAINLET_IMPLEMENTED)
#include "http.h"
#include <cstring> // std::strncmp
#include <stdexcept>
#include <iostream>
#include <sstream> // std::istringstream, std::ostringstream
#include <cstdio> // std::snprintf
#include <chrono>
#include <stdexcept>
#include <errno.h>
#include <cassert>
#include <cstdlib> // std;:strtol
#include <algorithm> // std::for_each
#include <utility> // std::move
#include <string.h> // strerror
#include <json/json.h>
namespace Brainlet {
Client::Client(const std::string& domain) : domain(domain) {}
void Client::escapeJSON(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::escapeOutput(std::string& string) {
std::string result;
result.reserve(string.size());
std::for_each(string.begin(), string.end(), [&](const char character) {
switch (character) {
case '\b':
case '\x1f': result.push_back('?'); break;
default: result.push_back(character); break;
}
});
string = std::move(result);
}
void Client::processNextEvent() {
socket->run_until(
// Stop in 250 microseconds
std::chrono::system_clock::now() +
std::chrono::microseconds(250)
);
}
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");
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::string error = value.get("message", "Server did not specify error").asString();
escapeOutput(error);
std::cerr << "Error while logging in: " << error << "\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) {
escapeOutput(buffer);
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) {
escapeOutput(buffer);
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);
}
break;
}
case READY: {
switch (opcode) {
case EVENT_NEW_MESSAGE: {
Channel channel = {
.id = data["channel"]["_id"].asString(),
.name = data["channel"]["title"].asString()
};
MessageAuthor author = {
.id = data["author"]["_id"].asString(),
.username = data["author"]["username"].asString()
};
Message message = {
.author = author,
.message = data["content"].asString()
};
if (client->onNewMessage)
client->onNewMessage(message, channel);
break;
}
default: {
escapeOutput(buffer);
std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n";
break;
}
}
break;
}
}
}
void Client::sendMessage(std::string message, std::string channelId) {
escapeJSON(message);
std::ostringstream bufferToSend;
bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})";
socket->send(bufferToSend.str());
}
}
#endif