essence-os/apps/irc_client.cpp

332 lines
9.9 KiB
C++

// This file is part of the Essence operating system.
// It is released under the terms of the MIT license -- see LICENSE.md.
// Written by: nakst.
// TODO Don't use EsTextbox for the output..
// TODO Put the connection settings in a Panel.Popup.
#define ES_INSTANCE_TYPE Instance
#include <essence.h>
struct Instance : EsInstance {
EsTextbox *textboxNick;
EsTextbox *textboxAddress;
EsTextbox *textboxPort;
EsTextbox *textboxOutput;
EsTextbox *textboxInput;
EsButton *buttonConnect;
EsThreadInformation networkingThread;
EsMutex inputCommandMutex;
char *inputCommand;
size_t inputCommandBytes;
};
const EsStyle styleSmallTextbox = {
.inherit = ES_STYLE_TEXTBOX_BORDERED_SINGLE,
.metrics = {
.mask = ES_THEME_METRICS_PREFERRED_WIDTH,
.preferredWidth = 100,
},
};
const EsStyle styleOutputTextbox = {
.inherit = ES_STYLE_TEXTBOX_NO_BORDER,
.metrics = {
.mask = ES_THEME_METRICS_FONT_FAMILY,
.fontFamily = ES_FONT_MONOSPACED,
},
};
const EsStyle styleInputTextbox = {
.inherit = ES_STYLE_TEXTBOX_BORDERED_SINGLE,
.metrics = {
.mask = ES_THEME_METRICS_FONT_FAMILY,
.fontFamily = ES_FONT_MONOSPACED,
},
};
int TextboxInputCallback(EsElement *element, EsMessage *message) {
Instance *instance = element->instance;
if (message->type == ES_MSG_KEY_DOWN) {
if (message->keyboard.scancode == ES_SCANCODE_ENTER) {
size_t inputCommandBytes = 0;
char *inputCommand = EsTextboxGetContents(instance->textboxInput, &inputCommandBytes);
if (inputCommandBytes) {
EsMutexAcquire(&instance->inputCommandMutex);
EsHeapFree(instance->inputCommand);
instance->inputCommand = inputCommand;
instance->inputCommandBytes = inputCommandBytes;
EsMutexRelease(&instance->inputCommandMutex);
} else {
EsHeapFree(inputCommand);
}
EsTextboxClear(instance->textboxInput, false);
return ES_HANDLED;
}
}
return 0;
}
void NetworkingThread(EsGeneric argument) {
Instance *instance = (Instance *) argument.p;
char errorMessage[4096];
size_t errorMessageBytes = 0;
char message[4096];
size_t messageBytes = 0;
char nick[64];
size_t nickBytes = 0;
char *password = nullptr;
size_t passwordBytes = 0;
char *address = nullptr;
size_t addressBytes = 0;
char buffer[1024];
uintptr_t bufferPosition = 0;
EsConnection connection = {};
connection.sendBufferBytes = 65536;
connection.receiveBufferBytes = 65536;
{
EsMessageMutexAcquire();
address = EsTextboxGetContents(instance->textboxAddress, &addressBytes);
EsMessageMutexRelease();
EsError error = EsAddressResolve(address, addressBytes, ES_FLAGS_DEFAULT, &connection.address);
if (error != ES_SUCCESS) {
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage),
"The address name '%s' could not be found.\n", addressBytes, address);
goto exit;
}
}
{
EsMessageMutexAcquire();
size_t portBytes;
char *port = EsTextboxGetContents(instance->textboxPort, &portBytes);
EsMessageMutexRelease();
connection.address.port = EsIntegerParse(port, portBytes);
EsHeapFree(port);
}
{
EsMessageMutexAcquire();
char *_nick = EsTextboxGetContents(instance->textboxNick, &nickBytes);
EsMessageMutexRelease();
if (nickBytes > sizeof(nick)) nickBytes = sizeof(nick);
EsMemoryCopy(nick, _nick, nickBytes);
EsHeapFree(_nick);
for (uintptr_t i = 0; i < nickBytes; i++) {
if (nick[i] == ':') {
password = nick + i + 1;
passwordBytes = nickBytes - i - 1;
nickBytes = i;
}
}
}
{
EsError error = EsConnectionOpen(&connection, ES_CONNECTION_OPEN_WAIT);
if (error != ES_SUCCESS) {
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage),
"Could not open the connection (%d).", error);
goto exit;
}
messageBytes = EsStringFormat(message, sizeof(message), "%z%s%zNICK %s\r\nUSER %s localhost %s :%s\r\n",
password ? "PASS " : "", passwordBytes, password, password ? "\r\n" : "",
nickBytes, nick, nickBytes, nick, addressBytes, address, nickBytes, nick);
EsConnectionWriteSync(&connection, message, messageBytes);
}
while (true) {
// TODO Ping the server every 2 minutes.
// TODO If we've received no messages for 5 minutes, timeout.
uintptr_t inputBytes = 0;
while (true) {
char *inputCommand = nullptr;
size_t inputCommandBytes = 0;
EsMutexAcquire(&instance->inputCommandMutex);
inputCommand = instance->inputCommand;
inputCommandBytes = instance->inputCommandBytes;
instance->inputCommand = nullptr;
instance->inputCommandBytes = 0;
EsMutexRelease(&instance->inputCommandMutex);
if (inputCommand) {
messageBytes = EsStringFormat(message, sizeof(message), "%s\r\n", inputCommandBytes, inputCommand);
EsConnectionWriteSync(&connection, message, messageBytes);
EsHeapFree(inputCommand);
}
size_t bytesRead;
EsError error = EsConnectionRead(&connection, buffer + bufferPosition, sizeof(buffer) - bufferPosition, &bytesRead);
if (error != ES_SUCCESS) {
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage), "The connection was lost (%d).", error);
goto exit;
}
bufferPosition += bytesRead;
if (bufferPosition >= 2) {
for (uintptr_t i = 0; i < bufferPosition - 1; i++) {
if (buffer[i] == '\r' && buffer[i + 1] == '\n') {
buffer[i] = 0;
inputBytes = i + 2;
goto gotMessage;
}
}
}
if (bufferPosition == sizeof(buffer)) {
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage), "The server sent an invalid message.");
goto exit;
}
}
gotMessage:;
EsMessageMutexAcquire();
EsTextboxInsert(instance->textboxOutput, buffer);
EsTextboxInsert(instance->textboxOutput, "\n");
EsMessageMutexRelease();
{
EsPrint("=================\n%z\n", buffer);
const char *command = nullptr, *user = nullptr, *parameters = nullptr, *text = nullptr;
char *position = buffer;
if (*position == ':') {
user = ++position;
while (*position && *position != ' ') position++;
if (*position) *position++ = 0;
} else {
user = address;
}
while (*position && *position == ' ') position++;
command = position;
while (*position && *position != ' ') position++;
if (*position) *position++ = 0;
while (*position && *position == ' ') position++;
if (*position != ':') {
parameters = position;
while (*position && *position != ' ') position++;
if (*position) *position++ = 0;
}
while (*position && *position == ' ') position++;
if (*position == ':') text = position + 1;
if (0 == EsCRTstrcmp(command, "PING")) {
messageBytes = EsStringFormat(message, sizeof(message), "PONG :%z\r\n", text ?: parameters);
EsConnectionWriteSync(&connection, message, messageBytes);
}
EsPrint("command: '%z'\nuser: '%z'\nparameters: '%z'\ntext: '%z'\n\n", command, user, parameters, text);
}
EsMemoryMove(buffer + inputBytes, buffer + bufferPosition, -inputBytes, false);
bufferPosition -= inputBytes;
}
exit:;
if (connection.handle) {
EsConnectionClose(&connection);
}
EsMessageMutexAcquire();
if (errorMessageBytes) {
EsDialogShow(instance->window, EsLiteral("Connection failed"), errorMessage, errorMessageBytes,
ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON);
}
EsElementSetDisabled(instance->textboxAddress, false);
EsElementSetDisabled(instance->textboxNick, false);
EsElementSetDisabled(instance->textboxPort, false);
EsElementSetDisabled(instance->textboxInput, true);
EsElementSetDisabled(instance->buttonConnect, false);
EsMessageMutexRelease();
EsHeapFree(address);
}
void ConnectCommand(Instance *instance, EsElement *, EsCommand *) {
EsElementSetDisabled(instance->textboxAddress, true);
EsElementSetDisabled(instance->textboxNick, true);
EsElementSetDisabled(instance->textboxPort, true);
EsElementSetDisabled(instance->textboxInput, false);
EsElementSetDisabled(instance->buttonConnect, true);
EsThreadCreate(NetworkingThread, &instance->networkingThread, instance);
}
void _start() {
_init();
while (true) {
EsMessage *message = EsMessageReceive();
if (message->type == ES_MSG_INSTANCE_CREATE) {
// Create an new instance.
Instance *instance = EsInstanceCreate(message, "IRC Client");
EsWindow *window = instance->window;
EsWindowSetIcon(window, ES_ICON_INTERNET_CHAT);
// Create the toolbar.
EsElement *toolbar = EsWindowGetToolbar(window);
EsPanel *section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Nick:"));
instance->textboxNick = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Address:"));
instance->textboxAddress = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Port:"));
instance->textboxPort = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
EsSpacerCreate(toolbar, ES_CELL_H_FILL);
instance->buttonConnect = EsButtonCreate(toolbar, ES_FLAGS_DEFAULT, 0, EsLiteral("Connect"));
EsButtonOnCommand(instance->buttonConnect, ConnectCommand);
// Create the main area.
EsPanel *panel = EsPanelCreate(window, ES_PANEL_VERTICAL | ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
instance->textboxOutput = EsTextboxCreate(panel, ES_CELL_FILL | ES_TEXTBOX_MULTILINE, EsStyleIntern(&styleOutputTextbox));
EsPanelCreate(panel, ES_CELL_H_FILL, ES_STYLE_SEPARATOR_HORIZONTAL);
EsPanel *inputArea = EsPanelCreate(panel, ES_PANEL_HORIZONTAL | ES_CELL_H_FILL, ES_STYLE_PANEL_FILLED);
instance->textboxInput = EsTextboxCreate(inputArea, ES_CELL_FILL, EsStyleIntern(&styleInputTextbox));
instance->textboxInput->messageUser = TextboxInputCallback;
EsElementSetDisabled(instance->textboxInput);
}
}
}