From b008298b0a606dac4bc078bf78fd2b047d2ee121 Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Thu, 18 Aug 2016 03:33:37 +0200 Subject: [PATCH] Add irc(1). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Juhani Krekelä --- Makefile | 1 + build-aux/config.mak | 21 + build-aux/tests/have-explicit_bzero.c | 8 + build-aux/tests/have-reallocarray.c | 8 + build-aux/tests/have-strlcat.c | 8 + build-aux/tests/have-strlcpy.c | 8 + irc/.gitignore | 4 + irc/Makefile | 56 ++ irc/compat.c | 65 ++ irc/compat.h | 40 + irc/connection.c | 549 ++++++++++++++ irc/connection.h | 102 +++ irc/database.c | 237 ++++++ irc/database.h | 73 ++ irc/irc.c | 1007 +++++++++++++++++++++++++ irc/network.h | 24 + irc/scrollback.c | 205 +++++ irc/scrollback.h | 82 ++ irc/string.c | 34 + irc/string.h | 28 + irc/ui.c | 545 +++++++++++++ irc/ui.h | 42 ++ 22 files changed, 3147 insertions(+) create mode 100644 build-aux/config.mak create mode 100644 build-aux/tests/have-explicit_bzero.c create mode 100644 build-aux/tests/have-reallocarray.c create mode 100644 build-aux/tests/have-strlcat.c create mode 100644 build-aux/tests/have-strlcpy.c create mode 100644 irc/.gitignore create mode 100644 irc/Makefile create mode 100644 irc/compat.c create mode 100644 irc/compat.h create mode 100644 irc/connection.c create mode 100644 irc/connection.h create mode 100644 irc/database.c create mode 100644 irc/database.h create mode 100644 irc/irc.c create mode 100644 irc/network.h create mode 100644 irc/scrollback.c create mode 100644 irc/scrollback.h create mode 100644 irc/string.c create mode 100644 irc/string.h create mode 100644 irc/ui.c create mode 100644 irc/ui.h diff --git a/Makefile b/Makefile index 23a04bd1..f57487f5 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ host \ hostname \ ifconfig \ init \ +irc \ kblayout \ kblayout-compiler \ login \ diff --git a/build-aux/config.mak b/build-aux/config.mak new file mode 100644 index 00000000..b8000c08 --- /dev/null +++ b/build-aux/config.mak @@ -0,0 +1,21 @@ +.PHONY: clean-tests +clean-tests: + rm -rf tests + rm -f config.h + +.PHONY: clean +clean: clean-tests + +config.h: $(addprefix tests/,$(addsuffix .h,$(TESTS))) + cat tests/*.h > config.h + +tests/%.h: ../build-aux/tests/%.c + @if [ ! -d tests ]; then mkdir -p tests; fi + @ln -sf ../../build-aux/tests/$*.c tests/$*.c + @if $(CC) $(CFLAGS) $(CPPFLAGS) -Werror=incompatible-pointer-types -c tests/$*.c -o /dev/null 2>tests/$*.log; then \ + echo "# tests/$*: Yes" && tail -n 1 $< > $@; \ + else \ + echo "# tests/$*: No" && true > $@; \ + fi + +-include ../build-aux/tests/*.d diff --git a/build-aux/tests/have-explicit_bzero.c b/build-aux/tests/have-explicit_bzero.c new file mode 100644 index 00000000..79c50e53 --- /dev/null +++ b/build-aux/tests/have-explicit_bzero.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + void (*ptr)(void*, size_t) = explicit_bzero; + return ptr ? 0 : 1; +} +#define HAVE_EXPLICIT_BZERO 1 diff --git a/build-aux/tests/have-reallocarray.c b/build-aux/tests/have-reallocarray.c new file mode 100644 index 00000000..10a732f3 --- /dev/null +++ b/build-aux/tests/have-reallocarray.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + void* (*ptr)(void*, size_t, size_t) = reallocarray; + return ptr ? 0 : 1; +} +#define HAVE_REALLOCARRAY 1 diff --git a/build-aux/tests/have-strlcat.c b/build-aux/tests/have-strlcat.c new file mode 100644 index 00000000..b3d3d56a --- /dev/null +++ b/build-aux/tests/have-strlcat.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + size_t (*ptr)(char*, const char*, size_t) = strlcat; + return ptr ? 0 : 1; +} +#define HAVE_STRLCAT 1 diff --git a/build-aux/tests/have-strlcpy.c b/build-aux/tests/have-strlcpy.c new file mode 100644 index 00000000..1ecb595a --- /dev/null +++ b/build-aux/tests/have-strlcpy.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + size_t (*ptr)(char*, const char*, size_t) = strlcpy; + return ptr ? 0 : 1; +} +#define HAVE_STRLCPY 1 diff --git a/irc/.gitignore b/irc/.gitignore new file mode 100644 index 00000000..dae38c8a --- /dev/null +++ b/irc/.gitignore @@ -0,0 +1,4 @@ +irc +*.o +config.h +tests diff --git a/irc/Makefile b/irc/Makefile new file mode 100644 index 00000000..8cf5c7c0 --- /dev/null +++ b/irc/Makefile @@ -0,0 +1,56 @@ +include ../build-aux/platform.mak +include ../build-aux/compiler.mak +include ../build-aux/version.mak +include ../build-aux/dirs.mak + +OPTLEVEL?=$(DEFAULT_OPTLEVEL) +CFLAGS?=$(OPTLEVEL) + +CFLAGS += -Wall -Wextra +CPPFLAGS += -DVERSIONSTR=\"$(VERSION)\" + +ifeq ($(HOST_IS_SORTIX),0) + CPPFLAGS+=-D_GNU_SOURCE +endif + +BINARY = irc +#MANPAGES1 = irc.1 + +OBJS=\ +compat.o \ +connection.o \ +database.o \ +irc.o \ +scrollback.o \ +string.o \ +ui.o \ + +all: $(BINARY) + +.PHONY: all install clean + +$(OBJS): config.h + +%.o: %.c + $(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + +$(BINARY): $(OBJS) + $(CC) $(CFLAGS) $(OBJS) -o $(BINARY) $(LIBS) + +install: all + mkdir -p $(DESTDIR)$(BINDIR) + install $(BINARY) $(DESTDIR)$(BINDIR) + #mkdir -p $(DESTDIR)$(MANDIR)/man1 + #install $(MANPAGES1) $(DESTDIR)$(MANDIR)/man1 + +clean: + rm -f $(BINARY) + rm -f $(OBJS) *.o + +TESTS=\ +have-explicit_bzero \ +have-reallocarray \ +have-strlcat \ +have-strlcpy \ + +include ../build-aux/config.mak diff --git a/irc/compat.c b/irc/compat.c new file mode 100644 index 00000000..c77d9f06 --- /dev/null +++ b/irc/compat.c @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * compat.c + * Compatibility. + */ + +#include +#include +#include +#include + +#include "config.h" +#include "compat.h" + +#ifndef HAVE_EXPLICIT_BZERO +void explicit_bzero(void* buffer, size_t size) +{ + memset(buffer, 0, size); +} +#endif + +#ifndef HAVE_REALLOCARRAY +void* reallocarray(void* ptr, size_t nmemb, size_t size) +{ + if ( size && nmemb && SIZE_MAX / size < nmemb ) + return errno = ENOMEM, (void*) NULL; + return realloc(ptr, nmemb * size); +} +#endif + +#ifndef HAVE_STRLCAT +size_t strlcat(char* restrict dest, const char* restrict src, size_t size) +{ + size_t dest_len = strnlen(dest, size); + if ( size <= dest_len ) + return dest_len + strlen(src); + return dest_len + strlcpy(dest + dest_len, src, size - dest_len); +} +#endif + +#ifndef HAVE_STRLCPY +size_t strlcpy(char* restrict dest, const char* restrict src, size_t size) +{ + if ( !size ) + return strlen(src); + size_t result; + for ( result = 0; result < size-1 && src[result]; result++ ) + dest[result] = src[result]; + dest[result] = '\0'; + return result + strlen(src + result); +} +#endif diff --git a/irc/compat.h b/irc/compat.h new file mode 100644 index 00000000..06fc9fb8 --- /dev/null +++ b/irc/compat.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * combat.h + * Compatibility. + */ + +#ifndef COMPAT_H +#define COMPAT_H + +#include + +#include "config.h" + +#ifndef HAVE_EXPLICIT_BZERO +void explicit_bzero(void*, size_t); +#endif +#ifndef HAVE_REALLOCARRAY +void* reallocarray(void*, size_t, size_t); +#endif +#ifndef HAVE_STRLCAT +size_t strlcat(char* restrict, const char* restrict, size_t); +#endif +#ifndef HAVE_STRLCPY +size_t strlcpy(char* restrict, const char* restrict, size_t); +#endif + +#endif diff --git a/irc/connection.c b/irc/connection.c new file mode 100644 index 00000000..f580ba21 --- /dev/null +++ b/irc/connection.c @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * connection.c + * IRC protocol. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "connection.h" + +void dump_error(const char* message, + size_t message_size, + const struct timespec* when) +{ + // TODO: Send the error somewhere appropriate in the UI. + + fprintf(stderr, "\e[91m"); + struct tm tm; + gmtime_r(&when->tv_sec, &tm); + fprintf(stderr, "[%i-%02i-%02i %02i:%02i:%02i %09li] ", + tm.tm_year + 1900, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + when->tv_nsec); + for ( size_t i = 0; i < message_size; i++ ) + { + if ( message[i] == '\r' ) + continue; + else if ( message[i] == '\n' ) + continue; + else if ( (unsigned char) message[i] < 32 ) + { + fprintf(stderr, "\e[31m"); + fprintf(stderr, "\\x%02X", (unsigned char) message[i]); + fprintf(stderr, "\e[91m"); + } + fputc((unsigned char) message[i], stderr); + } + fprintf(stderr, "\e[m\n"); +} + +void dump_outgoing(const char* message, + size_t message_size, + const struct timespec* when) +{ + return; // TODO: Remove this, or adopt it for a logging mechanism. + + fprintf(stderr, "\e[92m"); + struct tm tm; + gmtime_r(&when->tv_sec, &tm); + fprintf(stderr, "[%i-%02i-%02i %02i:%02i:%02i %09li] ", + tm.tm_year + 1900, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + when->tv_nsec); + for ( size_t i = 0; i < message_size; i++ ) + { + if ( message[i] == '\r' ) + continue; + else if ( message[i] == '\n' ) + continue; + else if ( (unsigned char) message[i] < 32 ) + { + fprintf(stderr, "\e[91m"); + fprintf(stderr, "\\x%02X", (unsigned char) message[i]); + fprintf(stderr, "\e[92m"); + continue; + } + fputc((unsigned char) message[i], stderr); + } + fprintf(stderr, "\e[m\n"); +} + +void dump_incoming(const char* message, + size_t message_size, + const struct timespec* when) +{ + return; // TODO: Remove this, or adopt it for a logging mechanism. + + fprintf(stderr, "\e[93m"); + struct tm tm; + gmtime_r(&when->tv_sec, &tm); + fprintf(stderr, "[%i-%02i-%02i %02i:%02i:%02i %09li] ", + tm.tm_year + 1900, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + when->tv_nsec); + for ( size_t i = 0; i < message_size; i++ ) + { + if ( message[i] == '\r' ) + continue; + else if ( message[i] == '\n' ) + continue; + else if ( (unsigned char) message[i] < 32 ) + { + fprintf(stderr, "\e[91m"); + fprintf(stderr, "\\x%02X", (unsigned char) message[i]); + fprintf(stderr, "\e[93m"); + continue; + } + fputc((unsigned char) message[i], stderr); + } + fprintf(stderr, "\e[m\n"); +} + +void irc_error_vlinef(const char* format, va_list ap_orig) +{ + va_list ap; + + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + + va_copy(ap, ap_orig); + char* string; + if ( 0 <= vasprintf(&string, format, ap) ) + { + va_end(ap); + dump_error(string, strlen(string), &now); + free(string); + return; + } + va_end(ap); + + char buffer[512]; + va_copy(ap, ap_orig); + if ( 0 <= vsnprintf(buffer, sizeof(buffer), format, ap) ) + { + va_end(ap); + dump_error(buffer, strlen(buffer), &now); + dump_error("(vasprintf failed printing that line)", + strlen("(vasprintf failed printing that line)"), &now); + return; + } + va_end(ap); + + dump_error(format, strlen(format), &now); + dump_error("(vsnprintf failed printing format string)", + strlen("(vsnprintf failed printing that format string)"), &now); +} + +void irc_error_linef(const char* format, ...) +{ + va_list ap; + va_start(ap, format); + irc_error_vlinef(format, ap), + va_end(ap); +} + +void irc_transmit(struct irc_connection* irc_connection, + const char* message, + size_t message_size) +{ + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + if ( irc_connection->connectivity_error ) + return; + dump_outgoing(message, message_size, &now); + int fd = irc_connection->fd; + while ( message_size ) + { + ssize_t amount = 0; + amount = send(fd, message, message_size, MSG_NOSIGNAL); + if ( amount < 0 || amount == 0 ) + { + warn("send"); + irc_connection->connectivity_error = true; + return; + } + message += amount; + message_size -= amount; + } +} + +void irc_transmit_message(struct irc_connection* irc_connection, + const char* message, + size_t message_size) +{ + assert(2 <= message_size); + assert(message[message_size - 2] == '\r'); + assert(message[message_size - 1] == '\n'); + + char buffer[512]; + if ( 512 < message_size ) + { + memcpy(buffer, message, 510); + buffer[510] = '\r'; + buffer[511] = '\n'; + message = buffer; + message_size = 512; + } + + irc_transmit(irc_connection, message, message_size); + + explicit_bzero(buffer, sizeof(buffer)); +} + +void irc_receive_more_bytes(struct irc_connection* irc_connection) +{ + if ( irc_connection->connectivity_error ) + return; + int fd = irc_connection->fd; + char* buffer = irc_connection->incoming_buffer; + size_t buffer_size = sizeof(irc_connection->incoming_buffer); + size_t buffer_used = irc_connection->incoming_amount; + size_t buffer_free = buffer_size - buffer_used; + if ( buffer_free == 0 ) + return; + // TODO: Use non-blocking IO for transmitting as well so O_NONBLOCK can + // always be used. + // TODO: Use MSG_DONTWAIT when supported in Sortix. + int flags = fcntl(fd, F_GETFL); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + ssize_t amount = recv(fd, buffer + buffer_used, buffer_free, 0); + fcntl(fd, F_SETFL, flags); + if ( amount < 0 ) + { + if ( errno == EAGAIN || errno == EWOULDBLOCK ) + return; + warn("recv"); + irc_connection->connectivity_error = true; + } + else if ( amount == 0 ) + { + // TODO: Gracefully close the connection. + irc_connection->connectivity_error = true; + } + else + { + irc_connection->incoming_amount += amount; + } +} + +void irc_receive_pop_bytes(struct irc_connection* irc_connection, + char* buffer, + size_t count) +{ + assert(count <= irc_connection->incoming_amount); + memcpy(buffer, irc_connection->incoming_buffer, count); + explicit_bzero(irc_connection->incoming_buffer, count); + memmove(irc_connection->incoming_buffer, + irc_connection->incoming_buffer + count, + irc_connection->incoming_amount - count); + irc_connection->incoming_amount -= count; + explicit_bzero(irc_connection->incoming_buffer + irc_connection->incoming_amount, + count); +} + +bool irc_receive_message(struct irc_connection* irc_connection, + char message[512], + struct timespec* when) +{ + if ( irc_connection->connectivity_error ) + return false; + size_t message_usable = 0; + while ( message_usable < irc_connection->incoming_amount && + irc_connection->incoming_buffer[message_usable] != '\r' && + irc_connection->incoming_buffer[message_usable] != '\n' ) + message_usable++; + if ( message_usable < irc_connection->incoming_amount && + irc_connection->incoming_buffer[message_usable] == '\r') + { + message_usable++; + if ( message_usable < irc_connection->incoming_amount && + irc_connection->incoming_buffer[message_usable] == '\n' ) + { + message_usable++; + irc_receive_pop_bytes(irc_connection, message, message_usable); + assert(message[message_usable-2] == '\r'); + assert(message[message_usable-1] == '\n'); + message[message_usable-2] = '\0'; + message[message_usable-1] = '\0'; + // TODO: This is not always when the message did arrive. + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + dump_incoming(message, message_usable-2, &now); + *when = now; + return true; + } + else if ( message_usable < irc_connection->incoming_amount ) + { + // TODO: Handle bad newline sequence. + warnx("recv: bad IRC newline"); + irc_connection->connectivity_error = true; + return false; + } + } + else if ( message_usable < irc_connection->incoming_amount && + irc_connection->incoming_buffer[message_usable] == '\n' ) + { + // TODO: Handle bad newline sequence. + warnx("recv: bad IRC newline"); + irc_connection->connectivity_error = true; + return false; + } + if ( message_usable == 512 ) + { + // TODO: Handle untruncated lines from the server. + warnx("recv: overlong IRC line from server"); + irc_connection->connectivity_error = true; + return false; + } + return false; +} + +void irc_transmit_string(struct irc_connection* irc_connection, + const char* string) +{ + char message[512]; + strncpy(message, string, 510); + message[510] = '\0'; + message[511] = '\0'; + size_t string_truncated_length = strlen(message); + for ( size_t i = 0; i < string_truncated_length; i++ ) + { + if ( message[i] == '\r' ) + message[i] = ' '; + if ( message[i] == '\n' ) + message[i] = ' '; + } + message[string_truncated_length + 0] = '\r'; + message[string_truncated_length + 1] = '\n'; + size_t message_length = strnlen(message, 512); + irc_transmit_message(irc_connection, message, message_length); + explicit_bzero(message, sizeof(message)); +} + +__attribute__((format(printf, 2, 0))) +void irc_transmit_vformat(struct irc_connection* irc_connection, + const char* format, + va_list ap) +{ + char* string = NULL; + if ( vasprintf(&string, format, ap) < 0 ) + { + // TODO: Hmm, what do we do here. + warn("vasprintf"); + // TODO: Should the error condition be set? + return; + } + irc_transmit_string(irc_connection, string); + explicit_bzero(string, strlen(string)); + free(string); +} + +__attribute__((format(printf, 2, 3))) +void irc_transmit_format(struct irc_connection* irc_connection, + const char* format, + ...) +{ + va_list ap; + va_start(ap, format); + irc_transmit_vformat(irc_connection, format, ap); + va_end(ap); +} + +void irc_command_pass(struct irc_connection* irc_connection, + const char* password) +{ + irc_transmit_format(irc_connection, "PASS :%s", password); +} + +void irc_command_nick(struct irc_connection* irc_connection, + const char* nick) +{ + irc_transmit_format(irc_connection, "NICK :%s", nick); +} + +void irc_command_user(struct irc_connection* irc_connection, + const char* nick, + const char* local_hostname, + const char* server_hostname, + const char* real_name) +{ + // TODO: What if there are spaces in some of these fields? + irc_transmit_format(irc_connection, "USER %s %s %s :%s", + nick, local_hostname, server_hostname, real_name); +} + +void irc_command_join(struct irc_connection* irc_connection, + const char* channel) +{ + irc_transmit_format(irc_connection, "JOIN :%s", channel); +} + +void irc_command_part(struct irc_connection* irc_connection, + const char* channel) +{ + irc_transmit_format(irc_connection, "PART :%s", channel); +} + +void irc_command_privmsg(struct irc_connection* irc_connection, + const char* where, + const char* what) + +{ + // TODO: Ensure where is valid. + irc_transmit_format(irc_connection, "PRIVMSG %s :%s", where, what); +} + +void irc_command_privmsgf(struct irc_connection* irc_connection, + const char* where, + const char* what_format, + ...) + +{ + va_list ap; + va_start(ap, what_format); + char msg[512]; + vsnprintf(msg, sizeof(msg), what_format, ap); + irc_command_privmsg(irc_connection, where, msg); + va_end(ap); +} + +void irc_command_notice(struct irc_connection* irc_connection, + const char* where, + const char* what) + +{ + // TODO: Ensure where is valid. + irc_transmit_format(irc_connection, "NOTICE %s :%s", where, what); +} + +void irc_command_noticef(struct irc_connection* irc_connection, + const char* where, + const char* what_format, + ...) + +{ + va_list ap; + va_start(ap, what_format); + char msg[512]; + vsnprintf(msg, sizeof(msg), what_format, ap); + irc_command_notice(irc_connection, where, msg); + va_end(ap); +} + +void irc_command_kick(struct irc_connection* irc_connection, + const char* where, + const char* who, + const char* why) +{ + // TODO: Ensure where and who are valid. + if ( why ) + irc_transmit_format(irc_connection, "KICK %s %s :%s", where, who, why); + else + irc_transmit_format(irc_connection, "KICK %s %s", where, who); +} + +void irc_command_quit(struct irc_connection* irc_connection, + const char* message) +{ + if ( message ) + irc_transmit_format(irc_connection, "QUIT :%s", message); + else + irc_transmit_string(irc_connection, "QUIT"); + shutdown(irc_connection->fd, SHUT_WR); +} + +void irc_command_quit_malfunction(struct irc_connection* irc_connection, + const char* message) +{ + if ( message ) + irc_transmit_format(irc_connection, "QUIT :%s", message); + else + irc_transmit_string(irc_connection, "QUIT"); + shutdown(irc_connection->fd, SHUT_RDWR); +} + +void irc_parse_message_parameter(char* message, + char* parameters[16], + size_t* num_parameters_ptr) +{ + size_t num_parameters = 0; + while ( message[0] != '\0' ) + { + if ( message[0] == ':' || num_parameters == (16-1) -1 ) + { + message++; + parameters[num_parameters++] = message; + break; + } + + parameters[num_parameters++] = message; + + size_t usable = 0; + while ( message[usable] != '\0' && message[usable] != ' ' ) + usable++; + + char lc = message[usable]; + message[usable] = '\0'; + + if ( lc != '\0' ) + message += usable + 1; + else + message += usable; + } + *num_parameters_ptr = num_parameters; +} + +void irc_parse_who(char* full, const char** who, const char** whomask) +{ + size_t bangpos = strcspn(full, "!"); + if ( full[bangpos] == '!' ) + { + full[bangpos] = '\0'; + *who = full; + *whomask = full + bangpos + 1; + } + else + { + *who = full; + *whomask = ""; + } +} diff --git a/irc/connection.h b/irc/connection.h new file mode 100644 index 00000000..2fd14eed --- /dev/null +++ b/irc/connection.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * connection.h + * IRC protocol. + */ + +#ifndef CONNECTION_H +#define CONNECTION_H + +#include +#include +#include +#include + +struct irc_connection +{ + int fd; + bool connectivity_error; + char incoming_buffer[512]; + size_t incoming_amount; +}; + +__attribute__((format(printf, 1, 0))) +void irc_error_vlinef(const char* format, va_list ap); +__attribute__((format(printf, 1, 2))) +void irc_error_linef(const char* format, ...); +void irc_transmit(struct irc_connection* irc_connection, + const char* message, + size_t message_size); +void irc_transmit_message(struct irc_connection* irc_connection, + const char* message, + size_t message_size); +void irc_transmit_string(struct irc_connection* irc_connection, + const char* string); +__attribute__((format(printf, 2, 0))) +void irc_transmit_vformat(struct irc_connection* irc_connection, + const char* format, + va_list ap); +__attribute__((format(printf, 2, 3))) +void irc_transmit_format(struct irc_connection* irc_connection, + const char* format, + ...); +void irc_receive_more_bytes(struct irc_connection* irc_connection); +bool irc_receive_message(struct irc_connection* irc_connection, + char message[512], + struct timespec* when); +void irc_command_pass(struct irc_connection* irc_connection, + const char* password); +void irc_command_nick(struct irc_connection* irc_connection, + const char* nick); +void irc_command_user(struct irc_connection* irc_connection, + const char* nick, + const char* local_hostname, + const char* server_hostname, + const char* real_name); +void irc_command_join(struct irc_connection* irc_connection, + const char* channel); +void irc_command_part(struct irc_connection* irc_connection, + const char* channel); +void irc_command_privmsg(struct irc_connection* irc_connection, + const char* where, + const char* what); +__attribute__((format(printf, 3, 4))) +void irc_command_privmsgf(struct irc_connection* irc_connection, + const char* where, + const char* what_format, + ...); +void irc_command_notice(struct irc_connection* irc_connection, + const char* where, + const char* what); +__attribute__((format(printf, 3, 4))) +void irc_command_noticef(struct irc_connection* irc_connection, + const char* where, + const char* what_format, + ...); +void irc_command_kick(struct irc_connection* irc_connection, + const char* where, + const char* who, + const char* why); +void irc_command_quit(struct irc_connection* irc_connection, + const char* message); +void irc_command_quit_malfunction(struct irc_connection* irc_connection, + const char* message); +void irc_parse_message_parameter(char* message, + char* parameters[16], + size_t* num_parameters_ptr); +void irc_parse_who(char* full, const char** who, const char** whomask); + +#endif diff --git a/irc/database.c b/irc/database.c new file mode 100644 index 00000000..53ad3bde --- /dev/null +++ b/irc/database.c @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * database.c + * Data structure for keeping track of channels and people. + */ + +#include + +#include +#include +#include +#include +#include + +#include "database.h" +#include "network.h" +#include "string.h" + +struct channel* find_channel(const struct network* state, const char* channel_name) +{ + assert(channel_name); + + for ( struct channel* channel = state->channels; channel; channel = channel->next_channel ) + if ( strchannelcmp(channel->name, channel_name) == 0 ) + return channel; + return NULL; +} + +struct channel* add_channel(struct network* state, const char* channel_name) +{ + assert(channel_name); + + assert(!find_channel(state, channel_name)); + struct channel* channel = (struct channel*) calloc(sizeof(struct channel), 1); + if ( !channel ) + return NULL; + channel->name = strdup(channel_name); + if ( !channel->name ) + return free(channel), (struct channel*) NULL; + channel->people = NULL; + + channel->prev_channel = NULL; + channel->next_channel = state->channels; + if ( state->channels ) + state->channels->prev_channel = channel; + state->channels = channel; + + return channel; +} + +struct channel* get_channel(struct network* state, const char* channel_name) +{ + assert(channel_name); + + for ( struct channel* result = find_channel(state, channel_name); result; result = NULL ) + return result; + return add_channel(state, channel_name); +} + +void remove_channel(struct network* state, struct channel* channel) +{ + while ( channel->people ) + remove_person_from_channel(state, channel->people); + + if ( channel->prev_channel ) + channel->prev_channel->next_channel = channel->next_channel; + else + state->channels = channel->next_channel; + + if ( channel->next_channel ) + channel->next_channel->prev_channel = channel->prev_channel; + + free(channel->name); + free(channel); +} + +struct person* find_person(const struct network* state, const char* nick) +{ + assert(nick); + + for ( struct person* person = state->people; person; person = person->next_person ) + if ( strnickcmp(person->nick, nick) == 0 ) + return person; + return NULL; +} + +struct person* add_person(struct network* state, const char* nick) +{ + assert(nick); + + assert(!find_person(state, nick)); + struct person* person = (struct person*) calloc(sizeof(struct person), 1); + if ( !person ) + return NULL; + person->nick = strdup(nick); + if ( !person->nick ) + return free(person), (struct person*) NULL; + + person->prev_person = NULL; + person->next_person = state->people; + if ( state->people ) + state->people->prev_person = person; + state->people = person; + + return person; +} + +struct person* get_person(struct network* state, const char* nick) +{ + assert(nick); + + for ( struct person* result = find_person(state, nick); result; result = NULL ) + return result; + return add_person(state, nick); +} + +void remove_person(struct network* state, struct person* person) +{ + while ( person->channels ) + remove_person_from_channel(state, person->channels); + + if ( person->prev_person ) + person->prev_person->next_person = person->next_person; + else + state->people = person->next_person; + + if ( person->next_person ) + person->next_person->prev_person = person->prev_person; + + free(person->nick); + free(person); +} + +struct channel_person* find_person_in_channel(const struct network* state, const char* nick, const char* channel_name) +{ + assert(nick); + assert(channel_name); + + struct channel* channel = find_channel(state, channel_name); + if ( !channel ) + return NULL; + for ( struct channel_person* channel_person = channel->people; channel_person; channel_person = channel_person->next_person_in_channel ) + { + assert(channel_person->person); + assert(channel_person->person->nick); + if ( strnickcmp(channel_person->person->nick, nick) == 0 ) + return channel_person; + } + return NULL; +} + +struct channel_person* add_person_to_channel(struct network* state, struct person* person, struct channel* channel) +{ + assert(person); + assert(channel); + + assert(person->nick); + assert(channel->name); + + assert(!find_person_in_channel(state, person->nick, channel->name)); + struct channel_person* channel_person = (struct channel_person*) + calloc(sizeof(struct channel_person), 1); + if ( !channel_person ) + return NULL; + channel_person->channel = channel; + channel_person->person = person; + + channel_person->prev_channel_person = NULL; + channel_person->next_channel_person = state->channel_people; + if ( state->channel_people ) + state->channel_people->prev_channel_person = channel_person; + state->channel_people = channel_person; + + channel_person->prev_person_in_channel = NULL; + channel_person->next_person_in_channel = channel->people; + if ( channel->people ) + channel->people->prev_person_in_channel = channel_person; + channel->people = channel_person; + + channel_person->prev_channel_for_person = NULL; + channel_person->next_channel_for_person = person->channels; + if ( person->channels ) + person->channels->prev_channel_for_person = channel_person; + person->channels = channel_person; + + return channel_person; +} + +struct channel_person* get_person_in_channel(struct network* state, struct person* person, struct channel* channel) +{ + for ( struct channel_person* result = + find_person_in_channel(state, person->nick, channel->name); result; result = NULL ) + return result; + return add_person_to_channel(state, person, channel); +} + +void remove_person_from_channel(struct network* state, struct channel_person* channel_person) +{ + if ( state->channel_people != channel_person ) + channel_person->prev_channel_person->next_channel_person = channel_person->next_channel_person; + else + state->channel_people = channel_person->next_channel_person; + + if ( channel_person->next_channel_person ) + channel_person->next_channel_person->prev_channel_person = channel_person->prev_channel_person; + + if ( channel_person->channel->people != channel_person ) + channel_person->prev_person_in_channel->next_person_in_channel = channel_person->next_person_in_channel; + else + channel_person->channel->people = channel_person->next_person_in_channel; + + if ( channel_person->next_person_in_channel ) + channel_person->next_person_in_channel->prev_person_in_channel = channel_person->prev_person_in_channel; + + if ( channel_person->person->channels != channel_person ) + channel_person->prev_channel_for_person->next_channel_for_person = channel_person->next_channel_for_person; + else + channel_person->person->channels = channel_person->next_channel_for_person; + + if ( channel_person->next_channel_for_person ) + channel_person->next_channel_for_person->prev_channel_for_person = channel_person->prev_channel_for_person; + + free(channel_person); +} diff --git a/irc/database.h b/irc/database.h new file mode 100644 index 00000000..e42f360f --- /dev/null +++ b/irc/database.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * database.h + * Data structure for keeping track of channels and people. + */ + +#ifndef DATABASE_H +#define DATABASE_H + +struct channel; +struct channel_person; +struct network; +struct person; + +struct person +{ + struct person* prev_person; + struct person* next_person; + char* nick; + struct channel_person* channels; + bool always_observable; // myself or having private messaged me +}; + +struct channel_person +{ + struct channel_person* prev_channel_person; + struct channel_person* next_channel_person; + struct channel_person* prev_person_in_channel; + struct channel_person* next_person_in_channel; + struct channel_person* prev_channel_for_person; + struct channel_person* next_channel_for_person; + struct channel* channel; + struct person* person; + bool is_operator; + bool is_voiced; +}; + +struct channel +{ + struct channel* prev_channel; + struct channel* next_channel; + char* name; + char* topic; + struct channel_person* people; +}; + +struct channel* find_channel(const struct network* state, const char* channel_name); +struct channel* add_channel(struct network* state, const char* channel_name); +struct channel* get_channel(struct network* state, const char* channel_name); +void remove_channel(struct network* state, struct channel* channel); +struct person* find_person(const struct network* state, const char* nick); +struct person* add_person(struct network* state, const char* nick); +struct person* get_person(struct network* state, const char* nick); +void remove_person(struct network* state, struct person* person); +struct channel_person* find_person_in_channel(const struct network* state, const char* nick, const char* channel_name); +struct channel_person* add_person_to_channel(struct network* state, struct person* person, struct channel* channel); +struct channel_person* get_person_in_channel(struct network* state, struct person* person, struct channel* channel); +void remove_person_from_channel(struct network* state, struct channel_person* channel_person); + +#endif diff --git a/irc/irc.c b/irc/irc.c new file mode 100644 index 00000000..cdef2f8f --- /dev/null +++ b/irc/irc.c @@ -0,0 +1,1007 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * Copyright (c) 2022 Juhani 'nortti' Krekelä. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * irc.c + * IRC client. + */ + +#include +#include + +#include +#if defined(__sortix__) +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "connection.h" +#include "database.h" +#include "network.h" +#include "scrollback.h" +#include "string.h" +#include "ui.h" + +static const char* fix_where(const char* where, bool* op, bool* voice) +{ + while ( where[0] == '@' || where[0] == '+' ) + { + if ( where[0] == '+' ) + { + *op = true; + *voice = true; + } + if ( where[0] == '@' ) + { + *op = true; + } + where++; + } + return where; +} + +// This should not happen unless the database was faulty or the network is +// faulty (perhaps malicious). +static void database_prediction_mistake(struct network* state, int line) +{ + irc_error_linef("database_prediction_mistake() at %s:%i!", __FILE__, line); + (void) state; +} + +#define database_prediction_mistake(x) database_prediction_mistake(x, __LINE__) + +static void garbage_collect_people(struct network* state) +{ +again: + for ( struct person* person = state->people; person; person = person->next_person ) + { + if ( !person->channels && !person->always_observable ) + { + remove_person(state, person); + goto again; // TODO: This runs in squared time. + } + } +} + +void on_startup(struct network* state) +{ + (void) state; +} + +void on_shutdown(struct network* state) +{ + (void) state; +} + +void on_nick(struct network* state, const char* who, const char* whomask, + const char* newnick) +{ + (void) whomask; + if ( !strnickcmp(who, newnick) ) + return; // We don't care if nothing changed. + + struct person* person; + + // There shouldn't be anyone by the name newnick, let's ensure that. + if ( (person = find_person(state, newnick)) ) + { + database_prediction_mistake(state); + // There, unexpectedly, is such a person that doesn't exist and it's me. + if ( !strnickcmp(newnick, state->nick) ) + return irc_command_quit_malfunction(state->irc_connection, "network nonsense"); + // There isn't a newnick person, but we thought there was, let's forget. + remove_person(state, person); + } + + // Locate the person in the database to update the nick. + if ( (person = find_person(state, who)) ) + { + char* newnick_copy = strdup(newnick); + if ( !newnick_copy ) + return irc_command_quit_malfunction(state->irc_connection, "strdup failure"); + free(person->nick); + person->nick = newnick_copy; + for ( struct channel_person* cp = person->channels; + cp; + cp = cp->next_channel_for_person ) + { + struct scrollback* sb = get_scrollback(state, cp->channel->name); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", + "%s is now known as %s", who, newnick); + } + struct scrollback* sb = find_scrollback(state, who); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", + "%s is now known as %s", who, newnick); + } + + // Evidently there was someone with the old nick we didn't know about, but + // now there isn't, and there is someone with the new nick. + else + { + // But we don't actually care about that person, we don't know any + // channels in which that person resides, so that person would get + // garbage collected. This shouldn't happen. The next time that person + // does anything in a channel, we'll put that person in that channel. + database_prediction_mistake(state); + } + + // In case I changed my name, update it. + if ( !strnickcmp(who, state->nick) ) + { + char* newnick_copy = strdup(newnick); + if ( !newnick_copy ) + return irc_command_quit_malfunction(state->irc_connection, "strdup failure"); + free(state->nick); + state->nick = newnick_copy; + } +} + +void on_quit(struct network* state, const char* who, const char* whomask, + const char* reason) +{ + (void) whomask; + (void) reason; + if ( !strnickcmp(who, state->nick) ) + return; // We don't care about our own quit message. + + // Delete that person from our user database. + struct person* person; + if ( (person = find_person(state, who)) ) + { + for ( struct channel_person* cp = person->channels; + cp; + cp = cp->next_channel_for_person ) + { + struct scrollback* sb = get_scrollback(state, cp->channel->name); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", "%s has quit (%s)", + who, reason); + } + struct scrollback* sb = find_scrollback(state, who); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", "%s has quit (%s)", + who, reason); + remove_person(state, person); + } + else + { + // Oddly, we didn't know about that person. But that's okay, the person + // is now gone, and not knowing about that person is correct. + database_prediction_mistake(state); + } +} + +static +void on_as_if_join(struct network* state, const char* who, const char* where) +{ + // Check if I'm joining a channel. + if ( !strnickcmp(who, state->nick) ) + { + // In case we already thought we were in that channel. + if ( find_channel(state, where) ) + { + // I'm already in that channel according to the database. + database_prediction_mistake(state); + assert(find_person_in_channel(state, state->nick, where)); + return; + } + + struct channel* channel; + if ( !(channel = add_channel(state, where)) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + + return; + } + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + database_prediction_mistake(state); + + if ( !(channel = add_channel(state, where)) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + // Find the person. + struct person* person = get_person(state, who); + if ( !person ) + return irc_command_quit_malfunction(state->irc_connection, "get_person failure"); + + // Check if the person is already in that channel. + if ( find_person_in_channel(state, who, channel->name) ) + { + // We mistakenly already thought that person was in this channel. + database_prediction_mistake(state); + return; + } + + // Put the person in the channel. + if ( !add_person_to_channel(state, person, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel"); +} + +void on_join(struct network* state, const char* who, const char* whomask, + const char* where) +{ + (void) whomask; + + where = fix_where(where, NULL, NULL); + if ( where[0] != '#' ) + return; // Join on non-channel doesn't make sense. + + on_as_if_join(state, who, where); + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + { + int activity = ACTIVITY_NONTALK; + if ( !strnickcmp(who, state->nick) ) + activity = ACTIVITY_NONE; + scrollback_printf(sb, activity, "*", "%s (%s) has joined %s", + who, whomask, where); + } +} + +static +void on_as_if_part(struct network* state, const char* who, const char* where) +{ + where = fix_where(where, NULL, NULL); + + // Check if I'm parting a channel. + if ( !strnickcmp(who, state->nick) ) + { + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + // I'm parting a channel I didn't know I was in. Do nothing, as now + // my wrong belief became right. + database_prediction_mistake(state); + return; + } + + // Forget about the channel as I'm no longer there to observe it. + remove_channel(state, channel); + // Some people may no longer be observable. + garbage_collect_people(state); + + return; + } + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + // Someone parted a channel I didn't know I was in. + database_prediction_mistake(state); + + if ( !add_channel(state, where) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + // Find the person. + struct person* person = find_person(state, who); + if ( !person ) + { + // I didn't know about the person that just left a channel I'm in. But + // since we don't know about that person in any other channel, we'll + // garbage collect them here. + database_prediction_mistake(state); + return; + } + + struct channel_person* channel_person = find_person_in_channel(state, who, where); + if ( !channel_person ) + { + // I didn't think that person was in this channel. But that's true now, + // so I don't need to do anything. + database_prediction_mistake(state); + return; + } + + // Remove the person from the channel. + remove_person_from_channel(state, channel_person); + + // If the person no longer shares any channels with us, we won't be notified + // of quits or renames, and thus can't track that person's identity, so we + // garbage collect the person until it comes back in side. Some people are + // always obserable, such as ourselves or people that have engaged in a + // private message to us. + if ( !person->channels && !person->always_observable ) + remove_person(state, person); +} + +void on_part(struct network* state, const char* who, const char* whomask, + const char* where) +{ + (void) whomask; + + where = fix_where(where, NULL, NULL); + if ( where[0] != '#' ) + return; // Part on non-channel doesn't make sense. + + on_as_if_part(state, who, where); + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", "%s (%s) has left %s", + who, whomask, where); +} + +static +void on_evidently_exists(struct network* state, const char* who, + const char* whomask, const char* where) +{ + (void) whomask; + + if ( where[0] != '#' ) + { + // Find the person. + struct person* person = find_person(state, who); + if ( !person ) + { + if ( !(person = add_person(state, who)) ) + return irc_command_quit_malfunction(state->irc_connection, "get_person failure"); + } + + // The person has now privately messaged us, so assume this person is + // now always observable. + person->always_observable = true; + + return; + } + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + database_prediction_mistake(state); + + if ( !add_channel(state, where) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + // TODO: The following code makes the assumption that you must be in a + // channel to do particular things and that's a wrong assumption. + return; + + // Find the person. + struct person* person = find_person(state, who); + if ( !person ) + { + database_prediction_mistake(state); + + if ( !(person = add_person(state, who)) ) + return irc_command_quit_malfunction(state->irc_connection, "get_person failure"); + } + + // Check if the person is already in that channel. + if ( !find_person_in_channel(state, who, channel->name) ) + { + // We mistakenly already thought that person was in this channel. + database_prediction_mistake(state); + + // Put the person in the channel. + if ( !add_person_to_channel(state, person, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel"); + } +} + +void on_privmsg(struct network* state, const char* who, const char* whomask, + const char* where, const char* what) +{ + bool where_op = false; + bool where_voice = false; + where = fix_where(where, &where_op, &where_voice); + + on_evidently_exists(state, who, whomask, where); + + // TODO: Does this happen? How about if we message ourself? Do we want this + // in an IRC client? + if ( !strnickcmp(who, state->nick) ) + return; // We don't care about our own messages. + + if ( !strcmp(what, "\x01VERSION\x01") ) + { + struct utsname un; + uname(&un); +#if defined(__sortix__) + irc_command_noticef(state->irc_connection, who, + "VERSION %s irc %s %s", + BRAND_DISTRIBUTION_NAME, VERSIONSTR, + BRAND_RELEASE_TAGLINE); +#else + irc_command_noticef(state->irc_connection, who, + "VERSION Sortix irc %s on %s %s", + VERSIONSTR, un.sysname, un.release); +#endif + } + + const char* sbname = where[0] == '#' ? where : who; + struct scrollback* sb = get_scrollback(state, sbname); + if ( sb ) + { + // TODO: \x01ACTION foo\x01 support. + // TODO: Highlights and such. + scrollback_print(sb, ACTIVITY_TALK, who, what); + } +} + +void on_notice(struct network* state, const char* who, const char* whomask, + const char* where, const char* what) +{ + bool where_op = false; + bool where_voice = false; + where = fix_where(where, &where_op, &where_voice); + + on_evidently_exists(state, who, whomask, where); + + if ( !strnickcmp(who, state->nick) ) + return; // We don't care about our own messages. + + const char* sbname = where[0] == '#' ? where : who; + struct scrollback* sb = get_scrollback(state, sbname); + if ( sb ) + { + // TODO: \x01ACTION foo\x01 support. + // TODO: Highlights and such. + // TODO: Print as -who-. + scrollback_print(sb, ACTIVITY_TALK, who, what); + } +} + +void on_topic(struct network* state, const char* who, const char* whomask, + const char* where, const char* topic) +{ + where = fix_where(where, NULL, NULL); + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + database_prediction_mistake(state); + + if ( !(channel = add_channel(state, where)) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + (void) who; + (void) whomask; + + free(channel->topic); + channel->topic = strdup(topic); + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", + "%s has changed the topic to: %s", who, topic); +} + +void on_kick(struct network* state, const char* who, const char* whomask, + const char* where, const char* target, const char* reason) +{ + where = fix_where(where, NULL, NULL); + + on_evidently_exists(state, who, whomask, where); + + on_as_if_part(state, target, where); + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", "%s has kicked %s (%s)", + who, target, reason); +} + +void on_mode(struct network* state, const char* who, const char* whomask, + const char* where, const char* mode, const char* target) +{ + (void) whomask; + + where = fix_where(where, NULL, NULL); + + on_evidently_exists(state, who, whomask, where); + + struct channel_person* cp = find_person_in_channel(state, target, where); + if ( cp ) + { + bool set = true; + for ( size_t i = 0; mode[i]; i++ ) + { + switch ( mode[i] ) + { + case '-': set = false; break; + case '+': set = true; break; + case 'o': cp->is_operator = set; break; + case 'v': cp->is_voiced = set; break; + } + } + } + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONTALK, "*", "%s sets mode %s on %s", + who, mode, target); +} + +void on_332(struct network* state, const char* where, const char* topic) +{ + where = fix_where(where, NULL, NULL); + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + database_prediction_mistake(state); + + if ( !(channel = add_channel(state, where)) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + free(channel->topic); + channel->topic = strdup(topic); + + struct scrollback* sb = get_scrollback(state, where); + if ( sb ) + scrollback_printf(sb, ACTIVITY_NONE, "*", "Topic for %s is: %s", + where, topic); +} + +void on_353(struct network* state, const char* wheretype, const char* where, + const char* list) +{ + (void) wheretype; + where = fix_where(where, NULL, NULL); + + // Find the channel. + struct channel* channel; + if ( !(channel = find_channel(state, where)) ) + { + database_prediction_mistake(state); + + if ( !add_channel(state, where) ) + return irc_command_quit_malfunction(state->irc_connection, "add_channel failure"); + + // I must be in that channel. + struct person* self = find_person(state, state->nick); + assert(self); + if ( !add_person_to_channel(state, self, channel) ) + return irc_command_quit_malfunction(state->irc_connection, "add_person_to_channel failure"); + } + + char names[512]; + strlcpy(names, list, sizeof(names)); + char* names_input = names; + char* names_next = NULL; + char* name; + while ( (name = strtok_r(names_input, " ", &names_next)) ) + { + bool is_operator = false; + bool is_voiced = false; + if ( name[0] == '@' ) + name++, is_operator = true; + else if ( name[0] == '+' ) + name++, is_voiced = true; + + struct channel_person* channel_person = + get_person_in_channel(state, get_person(state, name), channel); + + channel_person->is_operator = is_operator; + channel_person->is_voiced = is_voiced; + + names_input = NULL; + } +} + +static bool handle_message(struct network* state, const char* orig_message) +{ + char message[512]; + memcpy(message, orig_message, sizeof(message)); + + char* parameters[16]; + size_t num_parameters; + irc_parse_message_parameter(message, parameters, &num_parameters); + + if ( 2 <= num_parameters && !strcmp(parameters[0], "PING") ) + { + irc_transmit_format(state->irc_connection, "PONG :%s", parameters[1]); + return true; + } + + if ( num_parameters != 1 ) + return false; + + irc_parse_message_parameter(parameters[0], parameters, &num_parameters); + + if ( num_parameters < 1 ) + return false; + + if ( !strcmp(parameters[1], "332") ) + { + if ( num_parameters < 5 ) + return false; + const char* where = parameters[3]; + const char* topic = parameters[4]; + on_332(state, where, topic); + return true; + } + + if ( !strcmp(parameters[1], "333") ) + { + // TODO: Topic set by. + return true; + } + + if ( !strcmp(parameters[1], "353") ) + { + if ( num_parameters < 6 ) + return false; + const char* wheretype = parameters[3]; + const char* where = parameters[4]; + const char* list = parameters[5]; + on_353(state, wheretype, where, list); + return true; + } + + if ( !strcmp(parameters[1], "366") ) + { + // TODO: End of /NAMES list. + return true; + } + + const char* who; + const char* whomask; + irc_parse_who(parameters[0], &who, &whomask); + + if ( num_parameters < 2 ) + return false; + + if ( num_parameters < 3 ) + return false; + + if ( !strcmp(parameters[1], "NICK") ) + { + const char* new_nick = parameters[2]; + on_nick(state, who, whomask, new_nick); + return true; + } + + if ( !strcmp(parameters[1], "QUIT") ) + { + const char* reason = parameters[2]; + on_quit(state, who, whomask, reason); + return true; + } + + const char* where = parameters[2]; + if ( !strnickcmp(where, state->nick) ) + where = who; + + if ( !strcmp(parameters[1], "JOIN") ) + { + on_join(state, who, whomask, where); + return true; + } + + if ( !strcmp(parameters[1], "PART") ) + { + on_part(state, who, whomask, where); + return true; + } + + if ( num_parameters < 4 ) + return false; + + if ( !strcmp(parameters[1], "PRIVMSG") ) + { + if ( strchr(who, '.') ) + return false; // Network message. + const char* what = parameters[3]; + on_privmsg(state, who, whomask, where, what); + return true; + } + + if ( !strcmp(parameters[1], "NOTICE") ) + { + if ( strchr(who, '.') ) + return false; // Network message. + const char* what = parameters[3]; + on_notice(state, who, whomask, where, what); + return true; + } + + if ( !strcmp(parameters[1], "TOPIC") ) + { + const char* topic = parameters[3]; + on_topic(state, who, whomask, where, topic); + return true; + } + + if ( num_parameters < 5 ) + return false; + + if ( !strcmp(parameters[1], "KICK") ) + { + const char* target = parameters[3]; + const char* reason = parameters[4]; + on_kick(state, who, whomask, where, target, reason); + return true; + } + + if ( !strcmp(parameters[1], "MODE") ) + { + const char* mode = parameters[3]; + const char* target = parameters[4]; + on_mode(state, who, whomask, where, mode, target); + return true; + } + + return false; +} + +static void on_message(struct network* state, const char* message) +{ + if ( !handle_message(state, message) ) + { + struct scrollback* sb = find_scrollback_network(state); + scrollback_print(sb, ACTIVITY_NONTALK, state->server_hostname, message); + } +} + +static void mainloop(struct network* state) +{ + struct ui ui; + ui_initialize(&ui, state); + + if ( state->password ) + { + irc_command_pass(state->irc_connection, state->password); + explicit_bzero(state->password, strlen(state->password)); + free(state->password); + } + irc_command_nick(state->irc_connection, state->nick); + irc_command_user(state->irc_connection, state->nick, "localhost", + state->server_hostname, state->real_name); + + struct person* self = add_person(state, state->nick); + if ( !self ) + { + irc_command_quit_malfunction(state->irc_connection, "add_person failure"); + return; + } + self->always_observable = true; + + if ( state->autojoin ) + { + irc_command_join(state->irc_connection, state->autojoin); + struct scrollback* sb = get_scrollback(state, state->autojoin); + if ( sb ) + ui.current = sb; + } + + on_startup(state); + + while ( true ) + { + ui_render(&ui); + + if ( state->irc_connection->connectivity_error ) + { + irc_error_linef("Exiting main loop due to transmit error"); + break; + } + + struct pollfd pfds[2]; + memset(pfds, 0, sizeof(pfds)); + pfds[0].fd = 0; + pfds[0].events = POLLIN; + pfds[1].fd = state->irc_connection->fd; + pfds[1].events = POLLIN; + + int status = poll(pfds, 2, -1); + if ( status < 0 ) + err(1, "poll"); + + if ( pfds[0].revents & POLLIN ) + { + char buffer[512]; + ssize_t amount = read(0, buffer, sizeof(buffer)); + if ( amount < 0 ) + err(1, "read: stdin"); + for ( ssize_t i = 0; i < amount; i++ ) + ui_input_char(&ui, buffer[i]); + } + if ( pfds[1].revents & POLLIN ) + { + irc_receive_more_bytes(state->irc_connection); + char message[512]; + struct timespec now; // TODO: Use this? + while ( irc_receive_message(state->irc_connection, message, &now) ) + on_message(state, message); + } + } + + on_shutdown(state); + + irc_command_quit(state->irc_connection, NULL); + + ui_destroy(&ui); +} + +int main(int argc, char* argv[]) +{ + setlocale(LC_ALL, ""); + + const char* host = NULL; + const char* nick = NULL; + const char* real_name = NULL; + const char* service = "6667"; + const char* password = NULL; + const char* autojoin = NULL; + + int c; + while ( 0 <= (c = getopt(argc, argv, "h:j:n:N:p:P:")) ) + { + switch ( c ) + { + case 'h': host = optarg; break; + case 'j': autojoin = optarg; break; + case 'n': nick = optarg; break; + case 'N': real_name = optarg; break; + case 'p': service = optarg; break; + case 'P': password = optarg; break; + default: errx(1, "invalid option -- '%c'", optopt); + } + } + + if ( !nick ) + { + struct passwd* pwd = getpwuid(getuid()); + if ( !pwd ) + errx(1, "no -n nick option was passed"); + nick = pwd->pw_name; + if ( !real_name ) + { + // TODO: How should gecos be properly parsed? + size_t commapos = strcspn(pwd->pw_gecos, ","); + pwd->pw_gecos[commapos] = '\0'; + if ( pwd->pw_gecos[0] ) + real_name = pwd->pw_gecos; + } + } + + if ( !real_name ) + real_name = nick; + if ( !host ) + errx(1, "no -h host option was passed"); + if ( !service ) + errx(1, "no -p port/service option was passed"); + + struct network state; + memset(&state, 0, sizeof(state)); + state.nick = strdup(nick); + if ( !state.nick ) + err(1, "strdup"); + state.real_name = strdup(real_name); + if ( !state.real_name ) + err(1, "strdup"); + if ( password ) + { + state.password = strdup(password); + if ( !state.password ) + err(1, "strdup"); + explicit_bzero((char*) password, strlen(password)); + } + state.server_hostname = strdup(host); + if ( !state.server_hostname ) + err(1, "strdup"); + state.autojoin = autojoin; + + struct addrinfo addrinfo_hints; + memset(&addrinfo_hints, 0, sizeof(addrinfo_hints)); + addrinfo_hints.ai_flags = 0; + addrinfo_hints.ai_family = AF_UNSPEC; + addrinfo_hints.ai_socktype = SOCK_STREAM; + addrinfo_hints.ai_protocol = 0; + + struct addrinfo* addrinfo; + int ret; + if ( (ret = getaddrinfo(host, service, &addrinfo_hints, &addrinfo)) != 0 ) + errx(1, "could not resolve: %s: %s: %s", + host, service, gai_strerror(ret)); + + int fd = -1; + for ( struct addrinfo* info = addrinfo; info; info = info->ai_next ) + { + if ( (fd = socket(info->ai_family, info->ai_socktype | SOCK_CLOEXEC, + info->ai_protocol)) < 0 ) + { + warn("socket"); + continue; + } + + if ( connect(fd, info->ai_addr, info->ai_addrlen) < 0 ) + { + warn("connect"); + close(fd); + continue; + } + break; + } + if ( fd < 0 ) + errx(1, "unable to connect, exiting."); + + freeaddrinfo(addrinfo); + + struct irc_connection irc_connection; + memset(&irc_connection, 0, sizeof(irc_connection)); + irc_connection.fd = fd; + state.irc_connection = &irc_connection; + + if ( !add_scrollback(&state, state.server_hostname) ) + err(1, "add_scrollback: %s", state.server_hostname); + + mainloop(&state); + + close(irc_connection.fd); + + return 0; +} diff --git a/irc/network.h b/irc/network.h new file mode 100644 index 00000000..262ed974 --- /dev/null +++ b/irc/network.h @@ -0,0 +1,24 @@ +#ifndef NETWORK_H +#define NETWORK_H + +struct account; +struct channel; +struct channel_person; +struct person; + +struct network +{ + struct irc_connection* irc_connection; + struct account* accounts; + struct channel* channels; + struct person* people; + struct channel_person* channel_people; + struct scrollback* scrollbacks; + char* nick; + char* real_name; + char* password; + char* server_hostname; + const char* autojoin; +}; + +#endif diff --git a/irc/scrollback.c b/irc/scrollback.c new file mode 100644 index 00000000..9c17895e --- /dev/null +++ b/irc/scrollback.c @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * scrollback.c + * Ordered messages for display. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "network.h" +#include "scrollback.h" +#include "string.h" + +void message_free(struct message* msg) +{ + free(msg->who); + free(msg->what); +} + +void scrollback_free(struct scrollback* sb) +{ + if ( sb->network ) + { + if ( sb->scrollback_prev ) + sb->scrollback_prev->scrollback_next = sb->scrollback_next; + else + sb->network->scrollbacks = sb->scrollback_next; + if ( sb->scrollback_next ) + sb->scrollback_next->scrollback_prev = sb->scrollback_prev; + sb->scrollback_prev = NULL; + sb->scrollback_next = NULL; + sb->network = NULL; + } + for ( size_t i = 0; i < sb->messages_count; i++ ) + message_free(&sb->messages[i]); + free(sb->messages); + free(sb->name); + free(sb); +} + +struct scrollback* find_scrollback_network(const struct network* network) +{ + // TODO: The server hostname can be a valid nick, for instance if the + // hostname doesn't contain any dot characters. + for ( struct scrollback* sb = network->scrollbacks; + sb; + sb = sb->scrollback_next ) + { + if ( sb->name[0] == '#' ) + continue; + if ( !strnickcmp(network->server_hostname, sb->name) ) + return sb; + } + return NULL; +} + +struct scrollback* find_scrollback(const struct network* network, + const char* name) +{ + assert(name); + for ( struct scrollback* sb = network->scrollbacks; + sb; + sb = sb->scrollback_next ) + { + if ( name[0] == '#' && sb->name[0] == '#' ) + { + if ( strchannelcmp(name + 1, sb->name + 1) == 0 ) + return sb; + } + else if ( name[0] != '#' && sb->name[0] != '#' ) + { + if ( strnickcmp(name + 1, sb->name + 1) == 0 ) + return sb; + } + } + return NULL; +} + +struct scrollback* add_scrollback(struct network* network, const char* name) +{ + struct scrollback* sb = + (struct scrollback*) calloc(1, sizeof(struct scrollback)); + if ( !sb ) + return NULL; + if ( !(sb->name = strdup(name)) ) + return scrollback_free(sb), (struct scrollback*) NULL; + sb->network = network; + sb->scrollback_next = sb->network->scrollbacks; + if ( sb->scrollback_next ) + sb->scrollback_next->scrollback_prev = sb; + sb->network->scrollbacks = sb; + return sb; +} + +struct scrollback* get_scrollback(struct network* network, const char* name) +{ + struct scrollback* result = find_scrollback(network, name); + if ( result ) + return result; + return add_scrollback(network, name); +} + +bool scrollback_add_message(struct scrollback* sb, + enum activity activity, + const struct message* msg) +{ + if ( sb->messages_count == sb->messages_allocated ) + { + size_t new_allocated = 2 * sb->messages_allocated; + if ( new_allocated == 0 ) + new_allocated = 64; + struct message* new_messages = (struct message*) + reallocarray(sb->messages, new_allocated, sizeof(struct message)); + if ( !new_messages ) + return false; + sb->messages = new_messages; + sb->messages_allocated = new_allocated; + } + sb->messages[sb->messages_count++] = *msg; + size_t who_width = strlen(msg->who); // TODO: Unicode? + if ( sb->who_width < who_width ) + sb->who_width = who_width; + if ( sb->activity < activity ) + sb->activity = activity; + return true; +} + +static void message_timestamp(struct message* msg) +{ + struct tm tm; + time_t now = time(NULL); + localtime_r(&now, &tm); + msg->sec = tm.tm_sec; + msg->min = tm.tm_min; + msg->hour = tm.tm_hour; +} + +bool scrollback_print(struct scrollback* sb, + enum activity activity, + const char* who, + const char* what) +{ + struct message msg; + memset(&msg, 0, sizeof(msg)); + message_timestamp(&msg); + if ( (msg.who = strdup(who)) && + (msg.what = strdup(what)) && + scrollback_add_message(sb, activity, &msg) ) + return true; + message_free(&msg); + return false; +} + +bool scrollback_printf(struct scrollback* sb, + enum activity activity, + const char* who, + const char* whatf, + ...) +{ + struct message msg; + memset(&msg, 0, sizeof(msg)); + message_timestamp(&msg); + va_list ap; + va_start(ap, whatf); + int len = vasprintf(&msg.what, whatf, ap); + va_end(ap); + if ( (msg.who = strdup(who)) && + 0 <= len && + scrollback_add_message(sb, activity, &msg) ) + return true; + message_free(&msg); + return false; +} + +void scrollback_clear(struct scrollback* sb) +{ + for ( size_t i = 0; i < sb->messages_count; i++ ) + { + free(sb->messages[i].who); + free(sb->messages[i].what); + } + sb->messages_count = 0; + sb->messages_allocated = 0; + free(sb->messages); + sb->messages = NULL; +} diff --git a/irc/scrollback.h b/irc/scrollback.h new file mode 100644 index 00000000..623f5294 --- /dev/null +++ b/irc/scrollback.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * scrollback.c + * Ordered messages for display. + */ + +#ifndef SCROLLBACK_H +#define SCROLLBACK_H + +#include +#include + +struct network; + +enum activity +{ + ACTIVITY_NONE, + ACTIVITY_NONTALK, + ACTIVITY_TALK, + ACTIVITY_HIGHLIGHT, +}; + +struct message +{ + int hour; + int min; + int sec; + char* who; + char* what; +}; + +struct scrollback +{ + struct network* network; + struct scrollback* scrollback_prev; + struct scrollback* scrollback_next; + char* name; + struct message* messages; + size_t messages_count; + size_t messages_allocated; + size_t who_width; + enum activity activity; +}; + +void message_free(struct message* msg); +void scrollback_free(struct scrollback* sb); +struct scrollback* find_scrollback_network(const struct network* network); +struct scrollback* find_scrollback(const struct network* network, + const char* name); +struct scrollback* add_scrollback(struct network* network, + const char* name); +struct scrollback* get_scrollback(struct network* network, + const char* name); +bool scrollback_add_message(struct scrollback* sb, + enum activity activity, + const struct message* msg); +bool scrollback_print(struct scrollback* sb, + enum activity activity, + const char* who, + const char* what); +__attribute__((format(printf, 4, 5))) +bool scrollback_printf(struct scrollback* sb, + enum activity activity, + const char* who, + const char* whatf, + ...); +void scrollback_clear(struct scrollback* sb); + +#endif diff --git a/irc/string.c b/irc/string.c new file mode 100644 index 00000000..13f149b1 --- /dev/null +++ b/irc/string.c @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * string.c + * String utility functions and compatibility. + */ + +#include + +#include "string.h" + +// TODO: Implement this properly in accordance with IRC RFC rules. +int strchannelcmp(const char* a, const char* b) +{ + return strcasecmp(a, b); +} + +// TODO: Implement this properly in accordance with IRC RFC rules. +int strnickcmp(const char* a, const char* b) +{ + return strcasecmp(a, b); +} diff --git a/irc/string.h b/irc/string.h new file mode 100644 index 00000000..c9f28521 --- /dev/null +++ b/irc/string.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * string.h + * String utility functions and compatibility. + */ + +#ifndef STRING_H +#define STRING_H + +#include + +int strchannelcmp(const char* a, const char* b); +int strnickcmp(const char* a, const char* b); + +#endif diff --git a/irc/ui.c b/irc/ui.c new file mode 100644 index 00000000..b44cb196 --- /dev/null +++ b/irc/ui.c @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * ui.c + * User Interface. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "connection.h" +#include "network.h" +#include "scrollback.h" +#include "ui.h" + +struct cell +{ + wchar_t c; + int fgcolor; + int bgcolor; +}; + +static struct termios saved_termios; + +void tty_show(struct cell* cells, size_t cols, size_t rows) +{ + printf("\e[H"); + int fgcolor = -1; + int bgcolor = -1; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + for ( size_t r = 0; r < rows; r++ ) + { + for ( size_t c = 0; c < cols; c++ ) + { + struct cell* cell = &cells[r * cols + c]; + if ( fgcolor != cell->fgcolor ) + { + printf("\e[%im", cell->fgcolor); + fgcolor = cell->fgcolor; + } + if ( bgcolor != cell->bgcolor ) + { + printf("\e[%im", cell->bgcolor); + bgcolor = cell->bgcolor; + } + char mb[MB_CUR_MAX]; + size_t amount = wcrtomb(mb, cell->c, &ps); + if ( amount == (size_t) -1 ) + continue; + fwrite(mb, 1, amount, stdout); + } + if ( r + 1 != rows ) + printf("\n"); + } + fflush(stdout); +} + +void on_sigquit(int sig) +{ + // TODO: This is not async signal safe. + ui_destroy(NULL); + // TODO: Use sigaction so the handler only runs once. + //raise(sig); + (void) sig; + raise(SIGKILL); +} + +void ui_initialize(struct ui* ui, struct network* network) +{ + memset(ui, 0, sizeof(*ui)); + ui->network = network; + ui->current = find_scrollback_network(network); + struct winsize ws; + if ( ioctl(1, TIOCGWINSZ, &ws) < 0 ) + err(1, "stdout: ioctl: TIOCGWINSZ"); + if ( tcgetattr(0, &saved_termios) < 0 ) + err(1, "stdin: tcgetattr"); + struct termios tcattr; + memcpy(&tcattr, &saved_termios, sizeof(struct termios)); + tcattr.c_lflag &= ~(ECHO | ICANON | IEXTEN); + tcattr.c_iflag |= ICRNL | ISIG; + tcattr.c_cc[VMIN] = 1; + tcattr.c_cc[VTIME] = 0; + signal(SIGINT, SIG_IGN); + signal(SIGQUIT, on_sigquit); + tcsetattr(0, TCSADRAIN, &tcattr); + if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 ) + { + printf("\e[?1049h"); + fflush(stdout); + } +} + +void ui_destroy(struct ui* ui) +{ + // TODO. + (void) ui; + // TODO: This should be done in an atexit handler as well. + if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 ) + { + printf("\e[?1049l"); + fflush(stdout); + } + tcsetattr(0, TCSADRAIN, &saved_termios); +} + +void increment_offset(size_t* o_ptr, size_t* line_ptr, size_t cols) +{ + if ( (*o_ptr)++ == cols ) + { + *o_ptr = 0; + (*line_ptr)++; + } +} + +void ui_render(struct ui* ui) +{ + mbstate_t ps; + struct winsize ws; + if ( ioctl(1, TIOCGWINSZ, &ws) < 0 ) + err(1, "stdout: ioctl: TIOCGWINSZ"); + size_t cols = ws.ws_col; + size_t rows = ws.ws_row; + + struct cell* cells = calloc(sizeof(struct cell) * cols, rows); + if ( !cells ) + err(1, "calloc"); + + for ( size_t r = 0; r < rows; r++ ) + { + for ( size_t c = 0; c < cols; c++ ) + { + struct cell* cell = &cells[r * cols + c]; + cell->c = L' '; + cell->fgcolor = 0; + cell->bgcolor = 0; + } + } + + // TODO: What if the terminal isn't large enough? + struct scrollback* sb = ui->current; + + sb->activity = ACTIVITY_NONE; + + size_t title_from = 0; + size_t when_offset = 0; + size_t when_width = 2 + 1 + 2 + 1 + 2; + size_t who_offset = when_offset + when_width + 1; + size_t who_width = sb->who_width; + size_t div_offset = who_offset + who_width + 1; + size_t what_offset = div_offset + 2; + size_t what_width = cols - what_offset; + size_t input_width = cols; + + size_t input_num_lines = 1; + for ( size_t i = 0, o = 0; i < ui->input_used; i++ ) + { + wchar_t wc = ui->input[i]; + int w = wcwidth(wc); + if ( w < 0 || w == 0 ) + continue; + if ( input_width <= o ) + { + input_num_lines++; + o = 0; + } + o += w; + } + + char* title; + if ( asprintf(&title, "%s @ %s / %s", ui->network->nick, + ui->network->server_hostname, ui->current->name) < 0 ) + err(1, "asprintf"); + size_t title_len = strlen(title); + size_t title_how_many = cols < title_len ? cols : title_len; + size_t title_offset = (cols - title_how_many) / 2; + for ( size_t i = 0; i < title_how_many; i++ ) + { + char c = title[i]; + size_t cell_r = title_from; + size_t cell_c = title_offset + i; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = btowc((unsigned char) c); + } + free(title); + + size_t scrollbacks_from = title_from + 1; + size_t scrollbacks_lines = 1; + size_t scrollbacks_o = 0; + for ( struct scrollback* iter = ui->network->scrollbacks; + iter; + iter = iter->scrollback_next ) + { + if ( iter->scrollback_prev ) + { + increment_offset(&scrollbacks_o, &scrollbacks_lines, cols); + increment_offset(&scrollbacks_o, &scrollbacks_lines, cols); + } + for ( size_t i = 0; iter->name[i]; i++ ) + { + char c = iter->name[i]; + size_t cell_r = scrollbacks_from + (scrollbacks_lines - 1); + size_t cell_c = scrollbacks_o; + struct cell* cell = &cells[cell_r * cols + cell_c]; + int fgcolor = 0; + if ( iter == sb ) + fgcolor = 1; // TODO: Boldness should be its own property. + else if ( iter->activity == ACTIVITY_NONTALK ) + fgcolor = 31; + else if ( iter->activity == ACTIVITY_TALK ) + fgcolor = 91; + else if ( iter->activity == ACTIVITY_HIGHLIGHT ) + fgcolor = 94; + cell->c = btowc((unsigned char) c); + cell->fgcolor = fgcolor; + increment_offset(&scrollbacks_o, &scrollbacks_lines, cols); + } + } + + size_t horhigh_from = scrollbacks_from + scrollbacks_lines; + + for ( size_t c = 0; c < cols; c++ ) + { + size_t cell_r = horhigh_from; + size_t cell_c = c; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = c == div_offset ? L'┬' : L'─'; + } + + size_t sb_from = horhigh_from + 1; + + // TODO: What if the input is too big? + size_t input_bottom = rows - input_num_lines; + size_t input_offset = 0; + for ( size_t i = 0, o = 0, line = 0; i < ui->input_used; i++ ) + { + wchar_t wc = ui->input[i]; + int w = wcwidth(wc); + if ( w < 0 || w == 0 ) + continue; + if ( input_width <= o ) + { + line++; + o = 0; + } + // TODO: If 1 < w. + size_t cell_r = input_bottom + line; + size_t cell_c = input_offset + o; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = wc; + o += w; + } + + size_t horlow_from = input_bottom - 1; + + for ( size_t c = 0; c < cols; c++ ) + { + size_t cell_r = horlow_from; + size_t cell_c = c; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = c == div_offset ? L'┴' : L'─'; + } + + size_t sb_to = horlow_from; + + for ( size_t r = sb_to - 1; r != sb_from - 1; r-- ) + { + size_t cell_r = r; + size_t cell_c = div_offset; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = L'│'; + } + + for ( size_t r = sb_to - 1, m = sb->messages_count - 1; + r != (sb_from - 1) && m != SIZE_MAX; + r--, m-- ) + { + struct message* msg = &sb->messages[m]; + size_t num_lines = 1; + size_t max_lines = sb_from - r + 1; + memset(&ps, 0, sizeof(ps)); + for ( size_t i = 0, o = 0; msg->what[i]; ) + { + wchar_t wc; + size_t amount = mbrtowc(&wc, msg->what + i, SIZE_MAX, &ps); + if ( amount == (size_t) -1 || amount == (size_t) -2 ) + { + // TODO. + memset(&ps, 0, sizeof(ps)); + continue; + } + i += amount; + int w = wcwidth(wc); + if ( w < 0 || w == 0 ) + continue; + if ( what_width <= o ) + { + num_lines++; + o = 0; + } + o += w; + } + size_t how_many_lines = max_lines < num_lines ? max_lines : num_lines; + size_t first_line = num_lines - how_many_lines; + if ( 1 < how_many_lines ) + r -= how_many_lines - 1; + if ( first_line == 0 ) + { + char when[2 + 1 + 2 + 1 + 2 + 1 + 1]; + snprintf(when, sizeof(when), "%02i:%02i:%02i ", + msg->hour, msg->min, msg->sec); + for ( size_t i = 0; when[i]; i++ ) + { + size_t cell_r = r; + size_t cell_c = when_offset + i; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = btowc((unsigned char) when[i]); + } + memset(&ps, 0, sizeof(ps)); + size_t msg_who_width = strlen(msg->who); + size_t msg_who_how_many = who_width < msg_who_width ? who_width : msg_who_width; + size_t msg_who_first = msg_who_width - msg_who_how_many; + size_t msg_who_offset = who_width - msg_who_how_many; + for ( size_t i = 0; i < msg_who_how_many; i++ ) + { + char c = msg->who[msg_who_first + i]; + size_t cell_r = r; + size_t cell_c = who_offset + msg_who_offset + i; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = btowc((unsigned char) c); + } + } + for ( size_t i = 0, o = 0, line = 0; msg->what[i]; ) + { + wchar_t wc; + size_t amount = mbrtowc(&wc, msg->what + i, SIZE_MAX, &ps); + if ( amount == (size_t) -1 || amount == (size_t) -2 ) + { + // TODO. + memset(&ps, 0, sizeof(ps)); + continue; + } + i += amount; + int w = wcwidth(wc); + if ( w < 0 || w == 0 ) + continue; + if ( what_width <= o ) + { + line++; + o = 0; + } + // TODO: If 1 < w. + if ( first_line <= line ) + { + size_t cell_r = r + line - first_line; + size_t cell_c = what_offset + o; + struct cell* cell = &cells[cell_r * cols + cell_c]; + cell->c = wc; + } + o += w; + } + } + + (void) ui; + + tty_show(cells, cols, rows); + + free(cells); +} + +static bool is_command(const char* input, + const char* cmd, + const char** param) +{ + size_t cmdlen = strlen(cmd); + if ( strncmp(input, cmd, cmdlen) != 0 ) + return false; + if ( !input[cmdlen] ) + { + if ( param ) + *param = NULL; + return true; + } + if ( input[cmdlen] != ' ' ) + return false; + if ( !param ) + return false; + *param = input + cmdlen + 1; + return true; +} + +static bool is_command_param(const char* input, + const char* cmd, + const char** param) +{ + if ( !is_command(input, cmd, param) ) + return false; + if ( !*param ) + return false; // TODO: Help message in scrollback. + return true; +} + +void ui_input_char(struct ui* ui, char c) +{ + wchar_t wc; + size_t amount = mbrtowc(&wc, &c, 1, &ui->input_ps); + if ( amount == (size_t) -2 ) + return; + if ( amount == (size_t) -1 ) + { + // TODO. + memset(&ui->input_ps, 0, sizeof(ui->input_ps)); + return; + } + if ( wc == L'\b' || wc == 127 ) + { + if ( 0 < ui->input_used ) + ui->input_used--; + } + else if ( wc == L'\f' /* ^L */ ) + { + scrollback_clear(ui->current); + // TODO: Schedule full redraw? + } + else if ( wc == L'\n' ) + { + char input[4 * sizeof(ui->input) / sizeof(ui->input[0])]; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + const wchar_t* wcs = ui->input; + size_t amount = wcsnrtombs(input, &wcs, ui->input_used, sizeof(input), &ps); + ui->input_used = 0; + if ( amount == (size_t) -1 ) + return; + input[amount < sizeof(input) ? amount : amount - 1] = '\0'; + struct irc_connection* conn = ui->network->irc_connection; + const char* who = ui->network->nick; + const char* where = ui->current->name; + const char* param; + if ( input[0] == '/' && input[1] != '/' ) + { + if ( !input[1] ) + return; + if ( is_command_param(input, "/w", ¶m) || + is_command_param(input, "/window", ¶m) ) + { + struct scrollback* sb = find_scrollback(ui->network, param); + if ( sb ) + ui->current = sb; + } + else if ( is_command_param(input, "/query", ¶m) ) + { + if ( param[0] == '#' ) + return; // TODO: Help in scrollback. + struct scrollback* sb = get_scrollback(ui->network, param); + if ( sb ) + ui->current = sb; + } + else if ( is_command_param(input, "/join", ¶m) ) + { + irc_command_join(conn, param); + struct scrollback* sb = get_scrollback(ui->network, param); + if ( sb ) + ui->current = sb; + } + // TODO: Make it default to the current channel if any. + else if ( is_command_param(input, "/part", ¶m) ) + { + irc_command_part(conn, param); + } + else if ( is_command(input, "/quit", ¶m) ) + { + irc_command_quit(conn, param ? param : "Quiting"); + } + else if ( is_command_param(input, "/nick", ¶m) ) + { + irc_command_nick(conn, param); + } + else if ( is_command_param(input, "/raw", ¶m) ) + { + irc_transmit_string(conn, param); + } + else if ( is_command_param(input, "/me", ¶m) ) + { + scrollback_printf(ui->current, ACTIVITY_NONE, "*", "%s %s", + who, param); + irc_command_privmsgf(conn, where, "\x01""ACTION %s""\x01", + param); + } + else if ( is_command(input, "/clear", ¶m) ) + { + scrollback_clear(ui->current); + } + // TODO: /ban + // TODO: /ctcp + // TODO: /deop + // TODO: /devoice + // TODO: /kick + // TODO: /mode + // TODO: /op + // TODO: /quiet + // TODO: /topic + // TODO: /voice + else + { + scrollback_printf(ui->current, ACTIVITY_NONE, "*", + "%s :Unknown command", input + 1); + } + } + else + { + const char* what = input; + if ( what[0] == '/' ) + what++; + scrollback_print(ui->current, ACTIVITY_NONE, who, what); + irc_command_privmsg(conn, where, what); + } + } + else + { + if ( ui->input_used < sizeof(ui->input) / sizeof(ui->input[0]) ) + ui->input[ui->input_used++] = wc; + } +} diff --git a/irc/ui.h b/irc/ui.h new file mode 100644 index 00000000..12860eff --- /dev/null +++ b/irc/ui.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * ui.h + * User Interface. + */ + +#ifndef UI_H +#define UI_H + +#include + +struct network; +struct scrollback; + +struct ui +{ + struct network* network; + struct scrollback* current; + wchar_t input[512]; + size_t input_used; + mbstate_t input_ps; +}; + +void ui_initialize(struct ui* ui, struct network* network); +void ui_render(struct ui* ui); +void ui_input_char(struct ui* ui, char c); +void ui_destroy(struct ui* ui); + +#endif