2021-03-26 01:59:07 +02:00
|
|
|
// 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 authorId;
|
|
|
|
std::string message;
|
|
|
|
} Message;
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
std::string id;
|
|
|
|
std::string name;
|
|
|
|
std::string color; ///< Color represented in #rrggbb
|
|
|
|
} User;
|
|
|
|
|
2021-03-26 18:06:08 +02:00
|
|
|
class Client : public User {
|
2021-03-26 01:59:07 +02:00
|
|
|
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);
|
2021-03-26 18:06:08 +02:00
|
|
|
void encode(std::string& string); ///< Encodes a string to a JSON value
|
2021-03-26 01:59:07 +02:00
|
|
|
|
|
|
|
public:
|
|
|
|
std::function<void(Channel)> onNewChannel = nullptr;
|
|
|
|
std::function<void(Message, Channel)> onNewMessage = nullptr;
|
|
|
|
|
|
|
|
Client(const std::string& domain);
|
|
|
|
|
|
|
|
void processNextEvent();
|
2021-03-26 18:06:08 +02:00
|
|
|
void login(std::string name, std::string password);
|
|
|
|
void sendMessage(std::string message, std::string channelId);
|
2021-03-26 01:59:07 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
#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
|
2021-03-26 18:06:08 +02:00
|
|
|
#include <utility> // std::move
|
2021-03-26 01:59:07 +02:00
|
|
|
#include <string.h> // strerror
|
|
|
|
|
|
|
|
#include <json/json.h>
|
|
|
|
|
|
|
|
namespace Brainlet {
|
|
|
|
Client::Client(const std::string& domain) : domain(domain) {}
|
|
|
|
|
2021-03-26 18:06:08 +02:00
|
|
|
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);
|
|
|
|
}
|
2021-03-26 01:59:07 +02:00
|
|
|
void Client::processNextEvent() {
|
|
|
|
socket->run_until(
|
|
|
|
// Stop in 250 microseconds
|
|
|
|
std::chrono::system_clock::now() +
|
|
|
|
std::chrono::microseconds(250)
|
|
|
|
);
|
|
|
|
}
|
2021-03-26 18:06:08 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-03-26 01:59:07 +02:00
|
|
|
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::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);
|
2021-03-26 18:06:08 +02:00
|
|
|
break;
|
2021-03-26 01:59:07 +02:00
|
|
|
}
|
|
|
|
default: {
|
|
|
|
std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 18:06:08 +02:00
|
|
|
void Client::sendMessage(std::string message, std::string channelId) {
|
|
|
|
encode(message);
|
|
|
|
|
2021-03-26 01:59:07 +02:00
|
|
|
std::ostringstream bufferToSend;
|
|
|
|
bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})";
|
|
|
|
socket->send(bufferToSend.str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|