diff --git a/Applications/IRCClient/.gitignore b/Applications/IRCClient/.gitignore new file mode 100644 index 0000000000..1dec4248c2 --- /dev/null +++ b/Applications/IRCClient/.gitignore @@ -0,0 +1,3 @@ +*.o +*.d +IRCClient diff --git a/Applications/IRCClient/IRCAppWindow.cpp b/Applications/IRCClient/IRCAppWindow.cpp new file mode 100644 index 0000000000..00ac55ff93 --- /dev/null +++ b/Applications/IRCClient/IRCAppWindow.cpp @@ -0,0 +1,54 @@ +#include "IRCAppWindow.h" +#include "IRCSubWindow.h" +#include +#include + +IRCAppWindow::IRCAppWindow() + : GWindow() + , m_client("127.0.0.1", 6667) +{ + set_title(String::format("IRC Client: %s:%d", m_client.hostname().characters(), m_client.port())); + set_rect(200, 200, 600, 400); + setup_client(); + setup_widgets(); +} + +IRCAppWindow::~IRCAppWindow() +{ +} + +void IRCAppWindow::setup_client() +{ + m_client.on_connect = [this] { + m_client.join_channel("#test"); + }; + + m_client.on_query_message = [this] (const String& name) { + // FIXME: Update query view. + }; + + m_client.on_channel_message = [this] (const String& channel_name) { + // FIXME: Update channel view. + }; + + m_client.connect(); +} + +void IRCAppWindow::setup_widgets() +{ + auto* widget = new GWidget(nullptr); + widget->set_fill_with_background_color(true); + set_main_widget(widget); + widget->set_layout(make(Orientation::Horizontal)); + + auto* subwindow_list = new GListBox(widget); + subwindow_list->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + subwindow_list->set_preferred_size({ 120, 0 }); + subwindow_list->add_item("test1"); + subwindow_list->add_item("test2"); + subwindow_list->add_item("test3"); + + auto* container = new GWidget(widget); + + auto* subwindow = new IRCSubWindow("Server", container); +} diff --git a/Applications/IRCClient/IRCAppWindow.h b/Applications/IRCClient/IRCAppWindow.h new file mode 100644 index 0000000000..966a0d2ea4 --- /dev/null +++ b/Applications/IRCClient/IRCAppWindow.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "IRCClient.h" + +class IRCAppWindow : public GWindow { +public: + IRCAppWindow(); + virtual ~IRCAppWindow() override; + +private: + void setup_client(); + void setup_widgets(); + + IRCClient m_client; +}; diff --git a/Applications/IRCClient/IRCChannel.cpp b/Applications/IRCClient/IRCChannel.cpp new file mode 100644 index 0000000000..1c6bec841b --- /dev/null +++ b/Applications/IRCClient/IRCChannel.cpp @@ -0,0 +1,47 @@ +#include "IRCChannel.h" +#include "IRCClient.h" +#include +#include + +IRCChannel::IRCChannel(IRCClient& client, const String& name) + : m_client(client) + , m_name(name) + , m_log(IRCLogBuffer::create()) +{ +} + +IRCChannel::~IRCChannel() +{ +} + +Retained IRCChannel::create(IRCClient& client, const String& name) +{ + return adopt(*new IRCChannel(client, name)); +} + +void IRCChannel::add_member(const String& name, char prefix) +{ + for (auto& member : m_members) { + if (member.name == name) { + member.prefix = prefix; + return; + } + } + m_members.append({ name, prefix }); + dump(); +} + +void IRCChannel::add_message(char prefix, const String& name, const String& text) +{ + log().add_message(prefix, name, text); + dump(); +} + +void IRCChannel::dump() const +{ + printf("IRCChannel{%p}: %s\n", this, m_name.characters()); + for (auto& member : m_members) { + printf(" (%c)%s\n", member.prefix ? member.prefix : ' ', member.name.characters()); + } + log().dump(); +} diff --git a/Applications/IRCClient/IRCChannel.h b/Applications/IRCClient/IRCChannel.h new file mode 100644 index 0000000000..d43702320b --- /dev/null +++ b/Applications/IRCClient/IRCChannel.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "IRCLogBuffer.h" + +class IRCClient; + +class IRCChannel : public Retainable { +public: + static Retained create(IRCClient&, const String&); + ~IRCChannel(); + + bool is_open() const { return m_open; } + void set_open(bool b) { m_open = b; } + + String name() const { return m_name; } + + void add_member(const String& name, char prefix); + void remove_member(const String& name); + + void add_message(char prefix, const String& name, const String& text); + + void dump() const; + + const IRCLogBuffer& log() const { return *m_log; } + IRCLogBuffer& log() { return *m_log; } + +private: + IRCChannel(IRCClient&, const String&); + + IRCClient& m_client; + String m_name; + struct Member { + String name; + char prefix { 0 }; + }; + Vector m_members; + bool m_open { false }; + + Retained m_log; +}; diff --git a/Applications/IRCClient/IRCClient.cpp b/Applications/IRCClient/IRCClient.cpp new file mode 100644 index 0000000000..fcff869573 --- /dev/null +++ b/Applications/IRCClient/IRCClient.cpp @@ -0,0 +1,341 @@ +#include "IRCClient.h" +#include "IRCChannel.h" +#include "IRCQuery.h" +#include +#include +#include +#include +#include +#include + +enum IRCNumeric { + RPL_NAMREPLY = 353, + RPL_ENDOFNAMES = 366, +}; + +IRCClient::IRCClient(const String& address, int port) + : m_hostname(address) + , m_port(port) + , m_nickname("anon") +{ +} + +IRCClient::~IRCClient() +{ +} + +bool IRCClient::connect() +{ + if (m_socket_fd != -1) { + ASSERT_NOT_REACHED(); + } + + m_socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (m_socket_fd < 0) { + perror("socket"); + exit(1); + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + + addr.sin_family = AF_INET; + addr.sin_port = htons(m_port); + int rc = inet_pton(AF_INET, m_hostname.characters(), &addr.sin_addr); + if (rc < 0) { + perror("inet_pton"); + exit(1); + } + + printf("Connecting to %s...", m_hostname.characters()); + fflush(stdout); + rc = ::connect(m_socket_fd, (struct sockaddr*)&addr, sizeof(addr)); + if (rc < 0) { + perror("connect"); + exit(1); + } + printf("ok!\n"); + + m_notifier = make(m_socket_fd, GNotifier::Read); + m_notifier->on_ready_to_read = [this] (GNotifier&) { receive_from_server(); }; + + if (on_connect) + on_connect(); + + send_user(); + send_nick(); + return true; +} + +void IRCClient::receive_from_server() +{ + char buffer[4096]; + int nread = recv(m_socket_fd, buffer, sizeof(buffer) - 1, 0); + if (nread < 0) { + perror("recv"); + exit(1); + } + if (nread == 0) { + printf("IRCClient: Connection closed!\n"); + exit(1); + } + buffer[nread] = '\0'; +#if 0 + printf("Received: '%s'\n", buffer); +#endif + + for (int i = 0; i < nread; ++i) { + char ch = buffer[i]; + if (ch == '\r') + continue; + if (ch == '\n') { + process_line(); + m_line_buffer.clear_with_capacity(); + continue; + } + m_line_buffer.append(ch); + } +} + +void IRCClient::process_line() +{ +#if 0 + printf("Process line: '%s'\n", line.characters()); +#endif + Message msg; + Vector prefix; + Vector command; + Vector current_parameter; + enum { + Start, + InPrefix, + InCommand, + InStartOfParameter, + InParameter, + InTrailingParameter, + } state = Start; + + for (int i = 0; i < m_line_buffer.size(); ++i) { + char ch = m_line_buffer[i]; + switch (state) { + case Start: + if (ch == ':') { + state = InPrefix; + continue; + } + state = InCommand; + [[fallthrough]]; + case InCommand: + if (ch == ' ') { + state = InStartOfParameter; + continue; + } + command.append(ch); + continue; + case InPrefix: + if (ch == ' ') { + state = InCommand; + continue; + } + prefix.append(ch); + continue; + case InStartOfParameter: + if (ch == ':') { + state = InTrailingParameter; + continue; + } + state = InParameter; + [[fallthrough]]; + case InParameter: + if (ch == ' ') { + if (!current_parameter.is_empty()) + msg.arguments.append(String(current_parameter.data(), current_parameter.size())); + current_parameter.clear_with_capacity(); + state = InStartOfParameter; + continue; + } + current_parameter.append(ch); + continue; + case InTrailingParameter: + current_parameter.append(ch); + continue; + } + } + if (!current_parameter.is_empty()) + msg.arguments.append(String(current_parameter.data(), current_parameter.size())); + msg.prefix = String(prefix.data(), prefix.size()); + msg.command = String(command.data(), command.size()); + handle(msg); +} + +void IRCClient::send(const String& text) +{ + int rc = ::send(m_socket_fd, text.characters(), text.length(), 0); + if (rc < 0) { + perror("send"); + exit(1); + } +} + +void IRCClient::send_user() +{ + send(String::format("USER %s 0 * :%s\r\n", m_nickname.characters(), m_nickname.characters())); +} + +void IRCClient::send_nick() +{ + send(String::format("NICK %s\r\n", m_nickname.characters())); +} + +void IRCClient::send_pong(const String& server) +{ + send(String::format("PONG %s\r\n", server.characters())); + sleep(1); +} + +void IRCClient::join_channel(const String& channel_name) +{ + send(String::format("JOIN %s\r\n", channel_name.characters())); +} + +void IRCClient::handle(const Message& msg) +{ + printf("IRCClient::execute: prefix='%s', command='%s', arguments=%d\n", + msg.prefix.characters(), + msg.command.characters(), + msg.arguments.size() + ); + + int i = 0; + for (auto& arg : msg.arguments) { + printf(" [%d]: %s\n", i, arg.characters()); + ++i; + } + + bool is_numeric; + int numeric = msg.command.to_uint(is_numeric); + + if (is_numeric) { + switch (numeric) { + case RPL_NAMREPLY: + handle_namreply(msg); + return; + } + } + + if (msg.command == "PING") + return handle_ping(msg); + + if (msg.command == "JOIN") + return handle_join(msg); + + if (msg.command == "PRIVMSG") + return handle_privmsg(msg); +} + +bool IRCClient::is_nick_prefix(char ch) const +{ + switch (ch) { + case '@': + case '+': + case '~': + case '&': + case '%': + return true; + } + return false; +} + +void IRCClient::handle_privmsg(const Message& msg) +{ + if (msg.arguments.size() < 2) + return; + if (msg.prefix.is_empty()) + return; + auto parts = msg.prefix.split('!'); + auto sender_nick = parts[0]; + auto target = msg.arguments[0]; + + printf("handle_privmsg: sender_nick='%s', target='%s'\n", sender_nick.characters(), target.characters()); + + if (sender_nick.is_empty()) + return; + + char sender_prefix = 0; + if (is_nick_prefix(sender_nick[0])) { + sender_prefix = sender_nick[0]; + sender_nick = sender_nick.substring(1, sender_nick.length() - 1); + } + + { + auto it = m_channels.find(target); + if (it != m_channels.end()) { + (*it).value->add_message(sender_prefix, sender_nick, msg.arguments[1]); + if (on_channel_message) + on_channel_message(target); + return; + } + } + auto& query = ensure_query(sender_nick); + query.add_message(sender_prefix, sender_nick, msg.arguments[1]); + if (on_query_message) + on_query_message(target); +} + +IRCQuery& IRCClient::ensure_query(const String& name) +{ + auto it = m_queries.find(name); + if (it != m_queries.end()) + return *(*it).value; + auto query = IRCQuery::create(*this, name); + auto& query_reference = *query; + m_queries.set(name, query.copy_ref()); + return query_reference; +} + +void IRCClient::handle_ping(const Message& msg) +{ + if (msg.arguments.size() < 0) + return; + m_server_messages.enqueue(String::format("Ping? Pong! %s\n", msg.arguments[0].characters())); + send_pong(msg.arguments[0]); +} + +void IRCClient::handle_join(const Message& msg) +{ + if (msg.arguments.size() != 1) + return; + auto& channel_name = msg.arguments[0]; + auto it = m_channels.find(channel_name); + ASSERT(it == m_channels.end()); + auto channel = IRCChannel::create(*this, channel_name); + m_channels.set(channel_name, move(channel)); +} + +void IRCClient::handle_namreply(const Message& msg) +{ + printf("NAMREPLY:\n"); + if (msg.arguments.size() < 4) + return; + + auto& channel_name = msg.arguments[2]; + + auto it = m_channels.find(channel_name); + if (it == m_channels.end()) { + fprintf(stderr, "Warning: Got RPL_NAMREPLY for untracked channel %s\n", channel_name.characters()); + return; + } + auto& channel = *(*it).value; + + auto members = msg.arguments[3].split(' '); + for (auto& member : members) { + if (member.is_empty()) + continue; + char prefix = 0; + if (is_nick_prefix(member[0])) + prefix = member[0]; + channel.add_member(member, prefix); + } + + channel.dump(); +} diff --git a/Applications/IRCClient/IRCClient.h b/Applications/IRCClient/IRCClient.h new file mode 100644 index 0000000000..12f4fbe5ac --- /dev/null +++ b/Applications/IRCClient/IRCClient.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include + +class IRCChannel; +class IRCQuery; +class GNotifier; + +class IRCClient { +public: + IRCClient(const String& address, int port = 6667); + ~IRCClient(); + + bool connect(); + + String hostname() const { return m_hostname; } + int port() const { return m_port; } + + String nickname() const { return m_nickname; } + + void join_channel(const String&); + + bool is_nick_prefix(char) const; + + Function on_connect; + Function on_disconnect; + Function on_channel_message; + Function on_query_message; + Function on_server_message; + +private: + struct Message { + String prefix; + String command; + Vector arguments; + }; + + void receive_from_server(); + void send(const String&); + void send_user(); + void send_nick(); + void send_pong(const String& server); + void process_line(); + void handle_join(const Message&); + void handle_ping(const Message&); + void handle_namreply(const Message&); + void handle_privmsg(const Message&); + void handle(const Message&); + IRCQuery& ensure_query(const String& name); + + String m_hostname; + int m_port { 0 }; + int m_socket_fd { -1 }; + + String m_nickname; + Vector m_line_buffer; + OwnPtr m_notifier; + HashMap> m_channels; + HashMap> m_queries; + + CircularQueue m_server_messages; +}; diff --git a/Applications/IRCClient/IRCLogBuffer.cpp b/Applications/IRCClient/IRCLogBuffer.cpp new file mode 100644 index 0000000000..1f62aa031a --- /dev/null +++ b/Applications/IRCClient/IRCLogBuffer.cpp @@ -0,0 +1,28 @@ +#include "IRCLogBuffer.h" +#include +#include + +Retained IRCLogBuffer::create() +{ + return adopt(*new IRCLogBuffer); +} + +IRCLogBuffer::IRCLogBuffer() +{ +} + +IRCLogBuffer::~IRCLogBuffer() +{ +} + +void IRCLogBuffer::add_message(char prefix, const String& name, const String& text) +{ + m_messages.enqueue({ time(nullptr), prefix, name, text }); +} + +void IRCLogBuffer::dump() const +{ + for (auto& message : m_messages) { + printf("%u <%c%8s> %s\n", message.timestamp, message.prefix, message.sender.characters(), message.text.characters()); + } +} diff --git a/Applications/IRCClient/IRCLogBuffer.h b/Applications/IRCClient/IRCLogBuffer.h new file mode 100644 index 0000000000..3e59c7c51d --- /dev/null +++ b/Applications/IRCClient/IRCLogBuffer.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +class IRCLogBuffer : public Retainable { +public: + static Retained create(); + ~IRCLogBuffer(); + + struct Message { + time_t timestamp { 0 }; + char prefix { 0 }; + String sender; + String text; + }; + + int count() const { return m_messages.size(); } + const Message& at(int index) const { return m_messages.at(index); } + + void add_message(char prefix, const String& name, const String& text); + + void dump() const; + +private: + IRCLogBuffer(); + + CircularQueue m_messages; +}; diff --git a/Applications/IRCClient/IRCQuery.cpp b/Applications/IRCClient/IRCQuery.cpp new file mode 100644 index 0000000000..65ffd3deb1 --- /dev/null +++ b/Applications/IRCClient/IRCQuery.cpp @@ -0,0 +1,32 @@ +#include "IRCQuery.h" +#include "IRCClient.h" +#include +#include + +IRCQuery::IRCQuery(IRCClient& client, const String& name) + : m_client(client) + , m_name(name) + , m_log(IRCLogBuffer::create()) +{ +} + +IRCQuery::~IRCQuery() +{ +} + +Retained IRCQuery::create(IRCClient& client, const String& name) +{ + return adopt(*new IRCQuery(client, name)); +} + +void IRCQuery::dump() const +{ + printf("IRCQuery{%p}: %s\n", this, m_name.characters()); + log().dump(); +} + +void IRCQuery::add_message(char prefix, const String& name, const String& text) +{ + log().add_message(prefix, name, text); + dump(); +} diff --git a/Applications/IRCClient/IRCQuery.h b/Applications/IRCClient/IRCQuery.h new file mode 100644 index 0000000000..629c6fbbe3 --- /dev/null +++ b/Applications/IRCClient/IRCQuery.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "IRCLogBuffer.h" + +class IRCClient; + +class IRCQuery : public Retainable { +public: + static Retained create(IRCClient&, const String& name); + ~IRCQuery(); + + String name() const { return m_name; } + void add_message(char prefix, const String& name, const String& text); + + void dump() const; + + const IRCLogBuffer& log() const { return *m_log; } + IRCLogBuffer& log() { return *m_log; } + +private: + IRCQuery(IRCClient&, const String& name); + + IRCClient& m_client; + String m_name; + + Retained m_log; +}; diff --git a/Applications/IRCClient/IRCSubWindow.cpp b/Applications/IRCClient/IRCSubWindow.cpp new file mode 100644 index 0000000000..a150ae37fd --- /dev/null +++ b/Applications/IRCClient/IRCSubWindow.cpp @@ -0,0 +1,11 @@ +#include "IRCSubWindow.h" + +IRCSubWindow::IRCSubWindow(const String& name, GWidget* parent) + : GWidget(parent) + , m_name(name) +{ +} + +IRCSubWindow::~IRCSubWindow() +{ +} diff --git a/Applications/IRCClient/IRCSubWindow.h b/Applications/IRCClient/IRCSubWindow.h new file mode 100644 index 0000000000..c62afc133d --- /dev/null +++ b/Applications/IRCClient/IRCSubWindow.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class IRCSubWindow : public GWidget { +public: + explicit IRCSubWindow(const String& name, GWidget* parent); + virtual ~IRCSubWindow() override; + + String name() const { return m_name; } + void set_name(const String& name) { m_name = name; } + +private: + String m_name; +}; diff --git a/Applications/IRCClient/Makefile b/Applications/IRCClient/Makefile new file mode 100644 index 0000000000..bcf149a6ca --- /dev/null +++ b/Applications/IRCClient/Makefile @@ -0,0 +1,38 @@ +OBJS = \ + IRCClient.o \ + IRCChannel.o \ + IRCQuery.o \ + IRCLogBuffer.o \ + IRCAppWindow.o \ + IRCSubWindow.o \ + main.o + +APP = IRCClient + +STANDARD_FLAGS = -std=c++17 +WARNING_FLAGS = -Wextra -Wall -Wundef -Wcast-qual -Wwrite-strings -Wimplicit-fallthrough +FLAVOR_FLAGS = -fno-exceptions -fno-rtti +OPTIMIZATION_FLAGS = -Os +INCLUDE_FLAGS = -I../.. -I. -I../../LibC + +DEFINES = -DSERENITY -DSANITIZE_PTRS -DUSERLAND + +CXXFLAGS = -MMD -MP $(WARNING_FLAGS) $(OPTIMIZATION_FLAGS) $(FLAVOR_FLAGS) $(STANDARD_FLAGS) $(INCLUDE_FLAGS) $(DEFINES) +CXX = i686-pc-serenity-g++ +LD = i686-pc-serenity-ld +AR = i686-pc-serenity-ar +LDFLAGS = -L../../LibC -L../../LibGUI + +all: $(APP) + +$(APP): $(OBJS) + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -lc + +.cpp.o: + @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< + +-include $(OBJS:%.o=%.d) + +clean: + @echo "CLEAN"; rm -f $(APPS) $(OBJS) *.d + diff --git a/Applications/IRCClient/main.cpp b/Applications/IRCClient/main.cpp new file mode 100644 index 0000000000..4ed4ae1e21 --- /dev/null +++ b/Applications/IRCClient/main.cpp @@ -0,0 +1,15 @@ +#include "IRCClient.h" +#include +#include "IRCAppWindow.h" +#include + +int main(int argc, char** argv) +{ + GApplication app(argc, argv); + + IRCAppWindow app_window; + app_window.show(); + + printf("Entering main loop...\n"); + return app.exec(); +} diff --git a/Kernel/makeall.sh b/Kernel/makeall.sh index 5dd7f8c756..2929983813 100755 --- a/Kernel/makeall.sh +++ b/Kernel/makeall.sh @@ -32,6 +32,8 @@ $make_cmd -C ../Applications/TextEditor clean && \ $make_cmd -C ../Applications/TextEditor && \ $make_cmd -C ../Applications/About clean && \ $make_cmd -C ../Applications/About && \ +$make_cmd -C ../Applications/IRCClient clean && \ +$make_cmd -C ../Applications/IRCClient && \ $make_cmd clean &&\ $make_cmd && \ sudo ./sync.sh diff --git a/Kernel/sync.sh b/Kernel/sync.sh index 440bb946f3..26e348564c 100755 --- a/Kernel/sync.sh +++ b/Kernel/sync.sh @@ -85,6 +85,8 @@ cp -v ../Applications/FileManager/FileManager mnt/bin/FileManager cp -v ../Applications/ProcessManager/ProcessManager mnt/bin/ProcessManager cp -v ../Applications/About/About mnt/bin/About cp -v ../Applications/TextEditor/TextEditor mnt/bin/TextEditor +cp -v ../Applications/IRCClient/IRCClient mnt/bin/IRCClient +ln -s IRCClient mnt/bin/irc cp -v ../WindowServer/WindowServer mnt/bin/WindowServer cp -v kernel.map mnt/ sh sync-local.sh diff --git a/LibGUI/GWindow.h b/LibGUI/GWindow.h index 21328d224c..dfe69cb296 100644 --- a/LibGUI/GWindow.h +++ b/LibGUI/GWindow.h @@ -8,7 +8,7 @@ class GWidget; -class GWindow final : public GObject { +class GWindow : public GObject { public: GWindow(GObject* parent = nullptr); virtual ~GWindow() override;