diff options
Diffstat (limited to 'Applications/IRCClient')
-rw-r--r-- | Applications/IRCClient/.gitignore | 3 | ||||
-rw-r--r-- | Applications/IRCClient/IRCAppWindow.cpp | 54 | ||||
-rw-r--r-- | Applications/IRCClient/IRCAppWindow.h | 17 | ||||
-rw-r--r-- | Applications/IRCClient/IRCChannel.cpp | 47 | ||||
-rw-r--r-- | Applications/IRCClient/IRCChannel.h | 45 | ||||
-rw-r--r-- | Applications/IRCClient/IRCClient.cpp | 341 | ||||
-rw-r--r-- | Applications/IRCClient/IRCClient.h | 65 | ||||
-rw-r--r-- | Applications/IRCClient/IRCLogBuffer.cpp | 28 | ||||
-rw-r--r-- | Applications/IRCClient/IRCLogBuffer.h | 31 | ||||
-rw-r--r-- | Applications/IRCClient/IRCQuery.cpp | 32 | ||||
-rw-r--r-- | Applications/IRCClient/IRCQuery.h | 32 | ||||
-rw-r--r-- | Applications/IRCClient/IRCSubWindow.cpp | 11 | ||||
-rw-r--r-- | Applications/IRCClient/IRCSubWindow.h | 15 | ||||
-rw-r--r-- | Applications/IRCClient/Makefile | 38 | ||||
-rw-r--r-- | Applications/IRCClient/main.cpp | 15 |
15 files changed, 774 insertions, 0 deletions
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 <LibGUI/GListBox.h> +#include <LibGUI/GBoxLayout.h> + +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<GBoxLayout>(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 <LibGUI/GWindow.h> +#include <LibGUI/GWidget.h> +#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 <stdio.h> +#include <time.h> + +IRCChannel::IRCChannel(IRCClient& client, const String& name) + : m_client(client) + , m_name(name) + , m_log(IRCLogBuffer::create()) +{ +} + +IRCChannel::~IRCChannel() +{ +} + +Retained<IRCChannel> 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 <AK/AKString.h> +#include <AK/CircularQueue.h> +#include <AK/Vector.h> +#include <AK/Retainable.h> +#include <AK/RetainPtr.h> +#include "IRCLogBuffer.h" + +class IRCClient; + +class IRCChannel : public Retainable<IRCChannel> { +public: + static Retained<IRCChannel> 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<Member> m_members; + bool m_open { false }; + + Retained<IRCLogBuffer> 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 <LibGUI/GNotifier.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <unistd.h> +#include <stdio.h> + +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<GNotifier>(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<char> prefix; + Vector<char> command; + Vector<char> 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 <AK/AKString.h> +#include <AK/HashMap.h> +#include <AK/CircularQueue.h> +#include <AK/Function.h> + +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<void()> on_connect; + Function<void()> on_disconnect; + Function<void(const String& channel)> on_channel_message; + Function<void(const String& name)> on_query_message; + Function<void()> on_server_message; + +private: + struct Message { + String prefix; + String command; + Vector<String> 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<char> m_line_buffer; + OwnPtr<GNotifier> m_notifier; + HashMap<String, RetainPtr<IRCChannel>> m_channels; + HashMap<String, RetainPtr<IRCQuery>> m_queries; + + CircularQueue<String, 1024> 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 <stdio.h> +#include <time.h> + +Retained<IRCLogBuffer> 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 <AK/AKString.h> +#include <AK/CircularQueue.h> +#include <AK/Retainable.h> +#include <AK/RetainPtr.h> + +class IRCLogBuffer : public Retainable<IRCLogBuffer> { +public: + static Retained<IRCLogBuffer> 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<Message, 1000> 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 <stdio.h> +#include <time.h> + +IRCQuery::IRCQuery(IRCClient& client, const String& name) + : m_client(client) + , m_name(name) + , m_log(IRCLogBuffer::create()) +{ +} + +IRCQuery::~IRCQuery() +{ +} + +Retained<IRCQuery> 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 <AK/AKString.h> +#include <AK/CircularQueue.h> +#include <AK/Vector.h> +#include <AK/Retainable.h> +#include <AK/RetainPtr.h> +#include "IRCLogBuffer.h" + +class IRCClient; + +class IRCQuery : public Retainable<IRCQuery> { +public: + static Retained<IRCQuery> 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<IRCLogBuffer> 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 <LibGUI/GWidget.h> + +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 <LibGUI/GApplication.h> +#include "IRCAppWindow.h" +#include <stdio.h> + +int main(int argc, char** argv) +{ + GApplication app(argc, argv); + + IRCAppWindow app_window; + app_window.show(); + + printf("Entering main loop...\n"); + return app.exec(); +} |