aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format32
-rw-r--r--.gitignore11
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt294
-rw-r--r--LICENSE12
-rw-r--r--NEWS330
-rw-r--r--README.adoc210
-rw-r--r--common.c1096
-rw-r--r--config.h.in15
m---------liberty0
-rwxr-xr-xplugins/xB/calc241
-rwxr-xr-xplugins/xB/coin128
-rwxr-xr-xplugins/xB/eval312
-rwxr-xr-xplugins/xB/factoids177
-rwxr-xr-xplugins/xB/pomodoro502
-rwxr-xr-xplugins/xB/script2310
-rwxr-xr-xplugins/xB/seen160
-rwxr-xr-xplugins/xB/seen-import-xC.pl39
-rwxr-xr-xplugins/xB/youtube111
-rw-r--r--plugins/xC/auto-rejoin.lua48
-rw-r--r--plugins/xC/censor.lua90
-rw-r--r--plugins/xC/fancy-prompt.lua113
-rw-r--r--plugins/xC/last-fm.lua178
-rw-r--r--plugins/xC/ping-timeout.lua32
-rw-r--r--plugins/xC/prime.lua68
-rw-r--r--plugins/xC/slack.lua147
-rw-r--r--plugins/xC/thin-cursor.lua27
-rw-r--r--plugins/xC/utm-filter.lua66
-rwxr-xr-xtest50
-rwxr-xr-xtest-nick-colors26
-rwxr-xr-xtest-static14
-rw-r--r--xB.adoc104
-rw-r--r--xB.c2064
-rw-r--r--xC-gen-proto-c.awk325
-rw-r--r--xC-gen-proto-go.awk519
-rw-r--r--xC-gen-proto-js.awk223
-rw-r--r--xC-gen-proto.awk305
-rw-r--r--xC-proto206
-rw-r--r--xC.adoc127
-rw-r--r--xC.c15971
-rw-r--r--xC.webpbin0 -> 34548 bytes
-rwxr-xr-xxD-gen-replies.awk29
-rw-r--r--xD-replies93
-rw-r--r--xD.adoc53
-rw-r--r--xD.c4106
-rw-r--r--xF.c172
-rw-r--r--xF.svg36
-rw-r--r--xP.webpbin0 -> 43094 bytes
-rw-r--r--xP/.gitignore4
-rw-r--r--xP/Makefile18
-rw-r--r--xP/gen-ircfmt.awk89
-rw-r--r--xP/go.mod10
-rw-r--r--xP/go.sum62
-rw-r--r--xP/public/ircfmt.woff2bin0 -> 1240 bytes
-rw-r--r--xP/public/xP.css257
-rw-r--r--xP/public/xP.js1108
-rw-r--r--xP/xP.go299
57 files changed, 33022 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..27838ac
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,32 @@
+# clang-format is fairly limited, and these rules are approximate:
+# - array initializers can get terribly mangled with clang-format 12.0,
+# - sometimes it still aligns with space characters,
+# - struct name NL { NL ... NL } NL name; is unachievable.
+BasedOnStyle: GNU
+ColumnLimit: 80
+IndentWidth: 4
+TabWidth: 4
+UseTab: ForContinuationAndIndentation
+BreakBeforeBraces: Allman
+SpaceAfterCStyleCast: true
+AlignAfterOpenBracket: DontAlign
+AlignOperands: DontAlign
+AlignConsecutiveMacros: Consecutive
+AllowAllArgumentsOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
+IndentGotoLabels: false
+
+# IncludeCategories has some potential, but it may also break the build.
+# Note that the documentation says the value should be "Never".
+SortIncludes: false
+
+# This is a compromise, it generally works out aesthetically better.
+BinPackArguments: false
+
+# Unfortunately, this can't be told to align to column 40 or so.
+SpacesBeforeTrailingComments: 2
+
+# liberty-specific macro body wrappers.
+MacroBlockBegin: "BLOCK_START"
+MacroBlockEnd: "BLOCK_END"
+ForEachMacros: ["LIST_FOR_EACH"]
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba08178
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# Build files
+/build
+
+# Qt Creator files
+/CMakeLists.txt.user*
+/xK.config
+/xK.files
+/xK.creator*
+/xK.includes
+/xK.cflags
+/xK.cxxflags
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..fc28e82
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "liberty"]
+ path = liberty
+ url = https://git.janouch.name/p/liberty.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..d316c15
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,294 @@
+# Ubuntu 18.04 LTS and OpenBSD 6.4
+cmake_minimum_required (VERSION 3.10)
+project (xK VERSION 1.5.0
+ DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C)
+
+# Options
+option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
+option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
+option (WANT_XF "Build xF" OFF)
+
+# Moar warnings
+set (CMAKE_C_STANDARD 99)
+set (CMAKE_C_STANDARD_REQUIRED ON)
+set (CMAKE_C_EXTENSIONS OFF)
+
+if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
+ # -Wunused-function is pretty annoying here, as everything is static
+ set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
+endif ()
+
+# Version
+set (project_version "${PROJECT_VERSION}")
+
+# Try to append commit ID if it follows a version tag. It might be nicer if
+# we could also detect dirty worktrees but that's very hard to get right.
+# If we didn't need this for CPack, we could use add_custom_command to generate
+# a version source/include file.
+find_package (Git)
+set (git_head "${PROJECT_SOURCE_DIR}/.git/HEAD")
+if (GIT_FOUND AND EXISTS "${git_head}")
+ configure_file ("${git_head}" git-head.tag COPYONLY)
+ file (READ "${git_head}" git_head_content)
+ if (git_head_content MATCHES "^ref: ([^\r\n]+)")
+ set (git_ref "${PROJECT_SOURCE_DIR}/.git/${CMAKE_MATCH_1}")
+ if (EXISTS "${git_ref}")
+ configure_file ("${git_ref}" git-ref.tag COPYONLY)
+ endif ()
+ endif ()
+
+ execute_process (COMMAND ${GIT_EXECUTABLE} describe --tags --match v*
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
+ RESULT_VARIABLE git_describe_result
+ OUTPUT_VARIABLE git_describe OUTPUT_STRIP_TRAILING_WHITESPACE)
+ if (NOT git_describe_result)
+ string (REGEX REPLACE "^v" "" project_version "${git_describe}")
+ endif ()
+endif ()
+
+# Dashes make filenames confusing and upset packaging software
+string (REPLACE "-" "+" project_version_safe "${project_version}")
+
+# Dependencies
+set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
+include (AddThreads)
+
+find_package (PkgConfig REQUIRED)
+pkg_check_modules (libssl REQUIRED libssl libcrypto)
+list (APPEND project_libraries ${libssl_LIBRARIES})
+include_directories (${libssl_INCLUDE_DIRS})
+link_directories (${libssl_LIBRARY_DIRS})
+
+if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
+ # Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
+ # our POSIX version macros make it undefined
+ add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
+elseif (APPLE)
+ add_definitions (-D_DARWIN_C_SOURCE)
+endif ()
+
+# -lrt is only for glibc < 2.17
+# -liconv may or may not be a part of libc
+# -lm may or may not be a part of libc
+foreach (extra iconv rt m)
+ find_library (extra_lib_${extra} ${extra})
+ if (extra_lib_${extra})
+ list (APPEND project_libraries ${extra_lib_${extra}})
+ endif ()
+endforeach ()
+
+include (CheckCSourceRuns)
+set (CMAKE_REQUIRED_LIBRARIES ${project_libraries})
+get_property (CMAKE_REQUIRED_INCLUDES
+ DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY INCLUDE_DIRECTORIES)
+CHECK_C_SOURCE_RUNS ("#include <iconv.h>
+ int main () { return iconv_open (\"UTF-8//TRANSLIT\", \"ISO-8859-1\")
+ == (iconv_t) -1; }" ICONV_ACCEPTS_TRANSLIT)
+
+# Dependencies for xC
+pkg_check_modules (libffi REQUIRED libffi)
+list (APPEND xC_libraries ${libffi_LIBRARIES})
+include_directories (${libffi_INCLUDE_DIRS})
+link_directories (${libffi_LIBRARY_DIRS})
+
+# XXX: other Lua versions may be acceptable, don't know yet
+pkg_search_module (lua lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua>=5.3)
+option (WITH_LUA "Enable support for Lua plugins" ${lua_FOUND})
+
+if (WITH_LUA)
+ if (NOT lua_FOUND)
+ message (FATAL_ERROR "Lua library not found")
+ endif ()
+
+ list (APPEND xC_libraries ${lua_LIBRARIES})
+ include_directories (${lua_INCLUDE_DIRS})
+ link_directories (${lua_LIBRARY_DIRS})
+endif ()
+
+find_package (Curses)
+pkg_check_modules (ncursesw ncursesw)
+if (ncursesw_FOUND)
+ list (APPEND xC_libraries ${ncursesw_LIBRARIES})
+ include_directories (${ncursesw_INCLUDE_DIRS})
+elseif (CURSES_FOUND)
+ list (APPEND xC_libraries ${CURSES_LIBRARY})
+ include_directories (${CURSES_INCLUDE_DIR})
+else ()
+ message (SEND_ERROR "Curses not found")
+endif ()
+
+if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
+ message (SEND_ERROR "You have to choose either GNU Readline or libedit")
+elseif (WANT_READLINE)
+ pkg_check_modules (readline readline)
+
+ # OpenBSD's default readline is too old
+ if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
+ include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
+ list (APPEND xC_libraries ereadline)
+ elseif (readline_FOUND)
+ list (APPEND xC_libraries ${readline_LIBRARIES})
+ include_directories (${readline_INCLUDE_DIRS})
+ link_directories (${readline_LIBRARY_DIRS})
+ else ()
+ list (APPEND xC_libraries readline)
+ endif ()
+elseif (WANT_LIBEDIT)
+ pkg_check_modules (libedit REQUIRED libedit)
+ list (APPEND xC_libraries ${libedit_LIBRARIES})
+ include_directories (${libedit_INCLUDE_DIRS})
+endif ()
+
+# Generate a configuration file
+set (HAVE_READLINE "${WANT_READLINE}")
+set (HAVE_EDITLINE "${WANT_LIBEDIT}")
+set (HAVE_LUA "${WITH_LUA}")
+
+include (GNUInstallDirs)
+set (project_config ${PROJECT_BINARY_DIR}/config.h)
+configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${project_config})
+include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
+
+# Generate IRC replies--we need a custom target because of the multiple outputs
+add_custom_command (OUTPUT xD-replies.c xD.msg
+ COMMAND env LC_ALL=C awk
+ -f ${PROJECT_SOURCE_DIR}/xD-gen-replies.awk
+ ${PROJECT_SOURCE_DIR}/xD-replies > xD-replies.c
+ DEPENDS
+ ${PROJECT_SOURCE_DIR}/xD-gen-replies.awk
+ ${PROJECT_SOURCE_DIR}/xD-replies
+ COMMENT "Generating files from the list of server numerics")
+add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c)
+
+add_custom_command (OUTPUT xC-proto.c
+ COMMAND env LC_ALL=C awk
+ -f ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
+ -f ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
+ ${PROJECT_SOURCE_DIR}/xC-proto > xC-proto.c
+ DEPENDS
+ ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
+ ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
+ ${PROJECT_SOURCE_DIR}/xC-proto
+ COMMENT "Generating xC relay protocol code")
+add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
+
+# Build
+foreach (name xB xC xD)
+ add_executable (${name} ${name}.c ${project_config})
+ target_link_libraries (${name} ${project_libraries})
+ add_threads (${name})
+endforeach ()
+
+add_dependencies (xD replies)
+add_dependencies (xC replies xC-proto)
+target_link_libraries (xC ${xC_libraries})
+
+if (WANT_XF)
+ pkg_check_modules (x11 REQUIRED x11 xrender xft fontconfig)
+ include_directories (${x11_INCLUDE_DIRS})
+ link_directories (${x11_LIBRARY_DIRS})
+
+ add_executable (xF xF.c ${project_config})
+ add_dependencies (xF xC-proto)
+ target_link_libraries (xF ${x11_LIBRARIES} ${project_libraries})
+ add_threads (xF)
+endif ()
+
+# Tests
+include (CTest)
+if (BUILD_TESTING)
+ add_executable (test-xC $<TARGET_PROPERTY:xC,SOURCES>)
+ set_target_properties (test-xC PROPERTIES COMPILE_DEFINITIONS TESTING)
+ target_link_libraries (test-xC $<TARGET_PROPERTY:xC,LINK_LIBRARIES>)
+ add_threads (test-xC)
+ add_dependencies (test-xC replies)
+
+ add_test (NAME test-xC COMMAND test-xC)
+ add_test (NAME custom-static-analysis
+ COMMAND ${PROJECT_SOURCE_DIR}/test-static)
+endif ()
+
+# Various clang-based diagnostics, loads of fake positives and spam
+file (GLOB clang_tidy_sources *.c)
+set (clang_tidy_checks misc-* readability-*
+ -readability-braces-around-statements
+ -readability-named-parameter)
+string (REPLACE ";" "," clang_tidy_checks "${clang_tidy_checks}")
+
+set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
+add_custom_target (clang-tidy
+ COMMAND clang-tidy -p ${PROJECT_BINARY_DIR} -checks=${clang_tidy_checks}
+ ${clang_tidy_sources} 1>&2
+ USES_TERMINAL
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+
+# Installation
+install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
+install (DIRECTORY plugins/xB/
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS)
+install (DIRECTORY plugins/xC/
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/xC/plugins)
+
+# Generate documentation from text markup
+find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
+find_program (A2X_EXECUTABLE a2x)
+if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
+ message (WARNING "Neither asciidoctor nor a2x were found, "
+ "falling back to a substandard manual page generator")
+endif ()
+
+foreach (page xB xC xD)
+ set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
+ list (APPEND project_MAN_PAGES "${page_output}")
+ if (ASCIIDOCTOR_EXECUTABLE)
+ add_custom_command (OUTPUT ${page_output}
+ COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
+ -a release-version=${project_version}
+ -o "${page_output}"
+ "${PROJECT_SOURCE_DIR}/${page}.adoc"
+ DEPENDS ${page}.adoc
+ COMMENT "Generating man page for ${page}" VERBATIM)
+ elseif (A2X_EXECUTABLE)
+ add_custom_command (OUTPUT ${page_output}
+ COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
+ -a release-version=${project_version}
+ -D "${PROJECT_BINARY_DIR}"
+ "${PROJECT_SOURCE_DIR}/${page}.adoc"
+ DEPENDS ${page}.adoc
+ COMMENT "Generating man page for ${page}" VERBATIM)
+ else ()
+ set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
+ add_custom_command (OUTPUT ${page_output}
+ COMMAND env LC_ALL=C awk -f ${ASCIIMAN}
+ "${PROJECT_SOURCE_DIR}/${page}.adoc" > ${page_output}
+ DEPENDS ${page}.adoc ${ASCIIMAN}
+ COMMENT "Generating man page for ${page}" VERBATIM)
+ endif ()
+endforeach ()
+
+add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
+
+foreach (page ${project_MAN_PAGES})
+ string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
+ install (FILES "${page}"
+ DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
+endforeach ()
+
+# CPack
+set (CPACK_PACKAGE_VERSION "${project_version_safe}")
+set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
+set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
+set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
+
+set (CPACK_GENERATOR "TGZ;ZIP")
+set (CPACK_PACKAGE_FILE_NAME
+ "${PROJECT_NAME}-${project_version_safe}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
+set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_version_safe}")
+
+set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
+set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
+set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_version_safe}")
+
+set (CPACK_SET_DESTDIR TRUE)
+include (CPack)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4b31682
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+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.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..1d3cd4a
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,330 @@
+2.0.0 (Unreleased)
+
+ * xD: implemented WALLOPS, choosing to make it target even non-operators
+
+ * xC: made it show WALLOPS messages, as PRIVMSG for the server buffer
+
+ * xC: all behaviour.* configuration options have been renamed to general.*,
+ with the exception of editor_command/editor, backlog_helper/pager,
+ and backlog_helper_strip_formatting/pager_strip_formatting
+
+ * xC: all attributes.* configuration options have been made abstract in
+ a subset of the git-config(1) format, and renamed to theme.*,
+ with the exception of attributes.reset, which has no replacement
+
+ * xC: replaced behaviour.save_on_quit with general.autosave
+
+ * xC: improved pager integration capabilities
+
+ * xC: unsolicited JOINs will no longer automatically activate the buffer
+
+ * xC: made Readline insert the longest common completion prefix first,
+ and prevented the possible-completions command from duplicating the prompt
+
+ * xC: normalized editline's history behaviour, making it a viable frontend
+
+ * xC: various bugfixes
+
+ * xC: added a relay interface, enabled through the general.relay_bind option
+
+ * Added a web frontend for xC called xP
+
+
+1.5.0 (2021-12-21) "The Show Must Go On"
+
+ * xC: made it possible to pass the cursor position to external editors,
+ in particular VIM and Emacs
+
+ * xC: started quoting text coming from bracketed pastes,
+ to minimize the risk of trying to execute filesystem paths as commands
+
+ * xC: fixed to work with post-2021-08-29 editline
+
+ * xC: extended editline's autocomplete to show all options
+
+ * utm-filter.lua: added Facebook's tracking parameter to the filter
+
+
+1.4.0 (2021-10-06) "Call Me Scruffy Scruffington"
+
+ * xC: made message autosplitting respect text formatting
+
+ * xC: fixed displaying IRC colours above 16
+
+ * xC: offer IRCnet as an IRC network to connect to,
+ rather than the lunatic new Freenode
+
+ * xD: started bumping the soft limit on file descriptors to the hard one
+
+
+1.3.0 (2021-08-07) "New World Order"
+
+ * xC: made nick autocompletion offer recent speakers first
+
+ * All binaries have been renamed to something even sillier,
+ and all references in the source tree have been redacted;
+ this represents a major incompatible change for all plugins;
+ configuration and program data have to be adjusted manually
+
+
+1.2.0 (2021-07-08) "There Are Other Countries As Well"
+
+ * xC: added a /squery command for IRCnet
+
+ * xC: added trivial support for SASL EXTERNAL, enabled by adding "sasl"
+ to the respective server's "capabilities" list
+
+ * xC: now supporting IRCv3.2 capability negotiation, including CAP DEL
+
+ * xC: added support for IRCv3 chghost
+
+ * xC: /deop and /devoice without arguments will use the client's user
+
+ * xC: /set +=/-= now treats its argument as a string array
+
+ * xC: made "/help /command" work the same way as "/help command" does
+
+ * xC: /ban and /unban don't mangle extended bans anymore
+
+ * xC: joining new channels no longer switches to their buffer automatically
+ if the current input line isn't empty
+
+ * censor.lua: now stripping colours from censored messages;
+ their attributes are also configurable rather than always black on black
+
+
+1.1.0 (2020-10-31) "What Do You Mean By 'This Isn't Germany'?"
+
+ * xC: made fancy-prompt.lua work with libedit
+
+ * xD: fixed a regression with an unspecified "bind_host"
+
+ * Miscellaneous minor improvements
+
+
+1.0.0 (2020-10-29) "We're Finally There!"
+
+ * Coming with real manual pages instead of help2man-generated stubs
+
+ * xC: added support for more IRC colours and strike-through text (M-m x)
+
+ * xC: now tolerating all UTF-8 messages cut off by the server
+
+ * xC: disabled "behaviour.backlog_helper_strip_formatting" by default
+ since the relevant issue with ACS terminfo entries has been resolved
+
+ * xC: enabled word wrapping in the backlog by default
+
+ * xC: made the unread marker span the whole line, with a configurable
+ character; the previous behaviour can be obtained by setting it empty
+
+ * xC: fixed the prompt not showing back up after exiting a backlog helper
+ when an external event has provoked an attempt to change it
+
+ * xC: now watching fellow channel users' away status when the server
+ supports the away-notify capability; indicated by italicised nicknames
+
+ * xC: added a plugin to highlight prime numbers in incoming messages
+
+ * xD: make sure an unspecified "bind_host" binds to both IPv4 and IPv6
+
+ * xB: install plugins to /usr/share and look for them in XDG data dirs
+
+ * Miscellaneous little fixes
+
+
+0.9.8 (2020-09-02) "Yep, Still Using It"
+
+ * xC: fixed a crash and prompt attribute output in libedit 20191231-3.1,
+ though users are officially discouraged from using this library
+
+ * xC: fixed Lua 5.4 build, so far the support is experimental
+
+ * Miscellaneous little fixes
+
+
+0.9.7 (2018-10-21) "Business as Usual"
+
+ * xD: fix wildcard handling in WHOIS
+
+ * xD: properly handle STATS without parametetrs
+
+ * xD: abort earlier when an invalid mode character is detected while
+ processing channel MODE messages
+
+ * xD: do not send NICK notifications when the nickname doesn't really change
+
+ * xD: fix hostname string verification (only used for "server_name")
+
+
+0.9.6 (2018-06-22) "I've Been Sitting Here All This Time"
+
+ * Code has been relicensed to 0BSD and moved to a private git hosting
+
+ * Fix LibreSSL compatibility
+
+ * xC: a second /disconnect cuts the connection by force
+
+ * xC: send a QUIT message to the IRC server on Ctrl-C
+
+ * xC: add a Slack plugin (even though the gateway's now defunct)
+
+ * xC: show an error message on log write failure
+
+ * xC: fix parsing of literal IPv6 addresses with port numbers
+
+ * xC: fix some error messages
+
+ * xC: workaround a Readline bug in the fancy-prompt.lua plugin
+
+ * xD: fix two memory leaks
+
+ * xD: improve error handling for incoming connections
+
+ * xD: disable TLS session reuse
+
+
+0.9.5 (2016-12-30) "It's Time"
+
+ * Better support for the KILL command
+
+ * xC: export many more fields to the Lua API, add a prompt hook
+
+ * xC: show channel user count in the prompt
+
+ * xC: allow hiding join/part messages and other noise (Meta-Shift-H)
+
+ * xC: allow autojoining channels with keys
+
+ * xC: rejoin channels with keys on reconnect
+
+ * xC: make /query without arguments just open the buffer
+
+ * xC: add a censor plugin
+
+ * xC: die on configuration parse errors
+
+ * xC: request channel modes also on rejoin
+
+ * xC: don't show remembered channel modes on parted channels
+
+ * xC: fix highlight detection in colored text
+
+ * xC: fix CTCP handling for the real world and don't decode X-QUOTEs
+
+ * xC: add support for OpenSSL 1.1.0
+
+
+0.9.4 (2016-04-28) "Oops"
+
+ * xC: fix crash on characters invalid in Windows-1252
+
+ * xC: add an auto-rejoin plugin
+
+ * xC: better date change messages with customizable formatting;
+ now also used in the backlog, so it looks closer to regular output
+
+ * xB: add a calc plugin providing a basic Scheme REPL
+
+ * xB: add a seen plugin
+
+ * xD, xB: use pledge(2) on OpenBSD
+
+
+0.9.3 (2016-03-27) "Doesn't Even Suck"
+
+ * Use TLS Server Name Indication when connecting to servers
+
+ * xC: now we erase the screen before displaying buffers
+
+ * xC: implemented word wrapping in buffers
+
+ * xC: added autocomplete for /topic
+
+ * xC: Lua API was improved and extended
+
+ * xC: added a basic last.fm "now playing" plugin
+
+ * xC: backlog limit was made configurable
+
+ * xC: allow changing the list of IRC capabilities to use if available
+
+ * xC: optimize buffer memory usage
+
+ * xC: added logging of messages sent from /quote and plugins
+
+ * xC: M-! and M-a to go to the next buffer in order with a highlight
+ or new activity respectively
+
+ * xC: added --format for previewing things like MOTD files
+
+ * xC: added /buffer goto supporting case insensitive partial matches
+
+ * xD: add support for IRCv3.2 server-time
+
+ * xB: plugins now run in a dedicated data directory
+
+ * xB: added a factoids plugin
+
+ * Remote addresses are now resolved asynchronously
+
+ * Various bugfixes
+
+
+0.9.2 (2015-12-31)
+
+ * xC: added rudimentary support for Lua scripting
+
+ * xC: added detection of pasting, so that it doesn't trigger other
+ keyboard shortcuts, such as for autocomplete
+
+ * xC: added auto-away capability
+
+ * xC: added an /oper command
+
+ * xC: libedit backend works again
+
+ * xC: added capability to edit the input line using VISUAL/EDITOR
+
+ * xC: added Meta-Tab to switch to the last used buffer
+
+ * xC: correctly respond to stopping and resuming (SIGTSTP)
+
+ * xC: fixed decoding of text formatting
+
+ * xC: unseen PMs now show up as highlights
+
+ * xC: various bugfixes
+
+
+0.9.1 (2015-09-25)
+
+ * All "ssl" options have been renamed to "tls"
+
+ * The project now builds on OpenBSD
+
+ * Pulled in kqueue support
+
+ * xC: added backlog/scrollback functionality using less(1)
+
+ * xC: made showing the entire set of channel mode user prefixes optional
+
+ * xC: nicknames in /names are now ordered
+
+ * xC: nicknames now use the 256-color terminal palette if available
+
+ * xC: now we skip entries in the "addresses" list that can't be resolved
+ to an address, along with displaying a more helpful message
+
+ * xC: joins, parts, nick changes and quits don't count as new buffer
+ activity anymore
+
+ * xC: added Meta-H to open the full log file
+
+ * xC: various bugfixes and little improvements
+
+
+0.9.0 (2015-07-23)
+
+ * Initial release
+
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..3c09ba7
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,210 @@
+xK
+==
+
+'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
+client, and a web frontend for the client. It's all you're ever going to
+need for chatting, so long as you can make do with slightly minimalist software.
+
+They're all lean on dependencies, and offer a maximally permissive licence.
+
+xC
+--
+The IRC client, and the core of 'xK'. It is largely defined by building on top
+of GNU Readline or BSD Editline that have been hacked to death. Its interface
+should feel somewhat familiar for weechat or irssi users.
+
+image::xC.webp[align="center"]
+
+It has most features you'd expect of an IRC client, such as being multiserver,
+a powerful configuration system, integrated help, text formatting, automatic
+message splitting, multiline editing, bracketed paste support, word wrapping
+that doesn't break links, autocomplete, logging, CTCP queries, auto-away,
+command aliases, SOCKS proxying, SASL EXTERNAL authentication using TLS client
+certificates, a remote relay interface, or basic support for Lua scripting.
+As a unique bonus, you can launch a full text editor from within.
+
+xP
+--
+The web frontend for 'xC', making use of its networked relay interface.
+It intentionally differs in that it uses a sans-serif font, and it shows
+the list of all buffers in a side panel. Otherwise it is a near replica,
+including link:xC.adoc#_key_bindings[keyboard shortcuts].
+
+image::xP.webp[align="center"]
+
+xD
+--
+The IRC daemon. It is designed for use as a regular user application rather
+than a system-wide daemon, and follows the XDG Base Directory Specification.
+If all you want is a decent, minimal IRCd for testing purposes or a small
+network of respectful users (or bots), this one will do it just fine.
+
+It autodetects TLS on incoming connections (I'm still wondering why everyone
+doesn't have this), authenticates operators via TLS client certificate
+fingerprints, and supports a number of IRCv3 capabilities.
+
+What it notably doesn't support is online changes to configuration, any limits
+besides the total number of connections and mode `+l`, or server linking
+(which also means no services).
+
+This program has been https://git.janouch.name/p/haven/src/branch/master/hid[
+ported to Go] in a different project, and development continues over there.
+
+xB
+--
+The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
+bot in C, it fairly quickly became a playground, and it eventually got me into
+writing the rest of this package.
+
+Its main characteristic is that it runs plugins as coprocesses, allowing for
+enhanced reliability and programming language freedom. Moreover, it recovers
+from any crashes, and offers native SOCKS support (even though socksify can add
+that easily to any program).
+
+Packages
+--------
+Regular releases are sporadic. git master should be stable enough. You can get
+a package with the latest development version from Archlinux's AUR.
+
+Building
+--------
+Build-only dependencies: CMake, pkg-config, awk, liberty (included),
+ asciidoctor or asciidoc (recommended but optional) +
+Common runtime dependencies: openssl +
+Additionally for 'xC': curses, libffi, readline >= 6.0 or libedit >= 2013-07-12,
+ lua >= 5.3 (optional) +
+
+ $ git clone --recursive https://git.janouch.name/p/xK.git
+ $ mkdir xK/build
+ $ cd xK/build
+ $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+ -DWANT_READLINE=ON -DWANT_LIBEDIT=OFF -DWITH_LUA=ON
+ $ make
+
+To install the application, you can do either the usual:
+
+ # make install
+
+Or you can try telling CMake to make a package for you:
+
+ $ cpack -G DEB # also supported: RPM, FreeBSD
+ # dpkg -i xK-*.deb
+
+Usage
+-----
+'xC' has in-program configuration. Just run it and read the instructions.
+Consult its link:xC.adoc[man page] for details about the interface.
+
+For the rest you might want to generate a configuration file:
+
+ $ xB --write-default-config
+ $ xD --write-default-config
+
+After making any necessary edits to the file (there are comments to aid you in
+doing that), simply run the appropriate program with no arguments:
+
+ $ xB
+ $ xD
+
+'xB' stays running in the foreground, therefore I recommend launching it inside
+a Screen or tmux session.
+
+'xD', on the other hand, immediately forks into the background. Use the PID
+file or something like `killall` if you want to terminate it. You can run it
+as a `forking` type systemd user service.
+
+xP
+~~
+The precondition for running 'xC' frontends is enabling its relay interface:
+
+ /set general.relay_bind = "127.0.0.1:9000"
+
+To build the web server, you'll need to install the Go compiler, and run `make`
+from the _xP_ directory. Then start it from the _public_ subdirectory,
+and navigate to the adress you gave it as its first argument--in the following
+example, that would be http://localhost:8080[]:
+
+ $ ../xP 127.0.0.1:8080 127.0.0.1:9000
+
+For remote use, it's recommended to put 'xP' behind a reverse proxy, with TLS,
+and some form of HTTP authentication. Pass the external URL of the WebSocket
+endpoint as the third command line argument in this case.
+
+Client Certificates
+-------------------
+'xC' will use the SASL EXTERNAL method to authenticate using the TLS client
+certificate specified by the respective server's `tls_cert` option if you add
+`sasl` to the `capabilities` option and the server supports this.
+
+'xD' uses SHA-1 fingerprints of TLS client certificates to authenticate users.
+To get the fingerprint from a certificate file in the required form, use:
+
+ $ openssl x509 -in public.pem -outform DER | sha1sum
+
+Custom Key Bindings in xC
+-------------------------
+The default and preferred frontend used in 'xC' is GNU Readline. This means
+that you can change your bindings by editing '~/.inputrc'. For example:
+
+....
+# Preload with system-wide settings
+$include /etc/inputrc
+
+# Make M-left and M-right reorder buffers
+$if xC
+"\e\e[C": move-buffer-right
+"\e\e[D": move-buffer-left
+$endif
+....
+
+Consult the source code and the GNU Readline manual for a list of available
+functions. Also refer to the latter for the exact syntax of this file.
+Beware that you can easily break the program if you're not careful.
+
+How do I make xC look like the screenshot?
+------------------------------------------
+With the defaults, 'xC' doesn't look too fancy because I don't want to have
+a hard dependency on either Lua for the bundled script that provides an easily
+adjustable enhanced prompt, or on 256-colour terminals. Moreover, it's nearly
+impossible to come up with a colour theme that would work well with both
+black-on-white and white-on-black terminals, or anything wild in between.
+
+Assuming that your build supports Lua plugins, and that you have a decent,
+properly set-up terminal emulator, it suffices to run:
+
+ /set general.pager = Press Tab here and change +Gb to +Gb1d
+ /set general.date_change_line = "%a %e %b %Y"
+ /set general.plugin_autoload += "fancy-prompt.lua"
+ /set theme.userhost = "109"
+ /set theme.join = "108"
+ /set theme.part = "138"
+ /set theme.external = "248"
+ /set theme.timestamp = "250 255"
+ /set theme.read_marker = "202"
+
+Configuration profiles
+----------------------
+Even though the applications don't directly support configuration profiles,
+they conform to the XDG standard, and thus you can change the location they
+load configuration from via XDG_CONFIG_HOME (normally '~/.config') and the
+location where store their data via XDG_DATA_HOME (normally '~/.local/share').
+
+It would be relatively easy to make the applications assume whatever name you
+run them under (for example by using symbolic links), and load different
+configurations accordingly, but I consider it rather messy and unnecessary.
+
+Contributing and Support
+------------------------
+Use https://git.janouch.name/p/xK to report any bugs, request features,
+or submit pull requests. `git send-email` is tolerated. If you want to discuss
+the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
+
+Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
+
+License
+-------
+This software is released under the terms of the 0BSD license, the text of which
+is included within the package along with the list of authors.
+
+Note that 'xC' becomes GPL-licensed when you link it against GNU Readline,
+but that is not a concern of this source package. The licenses are compatible.
diff --git a/common.c b/common.c
new file mode 100644
index 0000000..ac83776
--- /dev/null
+++ b/common.c
@@ -0,0 +1,1096 @@
+/*
+ * common.c: common functionality
+ *
+ * Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ */
+
+#define LIBERTY_WANT_SSL
+#define LIBERTY_WANT_ASYNC
+#define LIBERTY_WANT_POLLER
+#define LIBERTY_WANT_PROTO_IRC
+
+#ifdef WANT_SYSLOG_LOGGING
+#define print_fatal_data ((void *) LOG_ERR)
+#define print_error_data ((void *) LOG_ERR)
+#define print_warning_data ((void *) LOG_WARNING)
+#define print_status_data ((void *) LOG_INFO)
+#define print_debug_data ((void *) LOG_DEBUG)
+#endif // WANT_SYSLOG_LOGGING
+
+#include "liberty/liberty.c"
+#include <arpa/inet.h>
+#include <netinet/tcp.h>
+
+static void
+init_openssl (void)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || LIBRESSL_VERSION_NUMBER
+ SSL_library_init ();
+ // XXX: this list is probably not complete
+ atexit (EVP_cleanup);
+ SSL_load_error_strings ();
+ atexit (ERR_free_strings);
+#else
+ // Cleanup is done automatically via atexit()
+ OPENSSL_init_ssl (0, NULL);
+#endif
+}
+
+static char *
+gai_reconstruct_address (struct addrinfo *ai)
+{
+ char host[NI_MAXHOST] = {}, port[NI_MAXSERV] = {};
+ int err = getnameinfo (ai->ai_addr, ai->ai_addrlen,
+ host, sizeof host, port, sizeof port,
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (err)
+ {
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
+ return xstrdup ("?");
+ }
+ return format_host_port_pair (host, port);
+}
+
+static bool
+accept_error_is_transient (int err)
+{
+ // OS kernels may return a wide range of unforeseeable errors.
+ // Assuming that they're either transient or caused by
+ // a connection that we've just extracted from the queue.
+ switch (err)
+ {
+ case EBADF:
+ case EINVAL:
+ case ENOTSOCK:
+ case EOPNOTSUPP:
+ return false;
+ default:
+ return true;
+ }
+}
+
+/// Destructively tokenize an address into a host part, and a port part.
+/// The port is only overwritten if that part is found, allowing for defaults.
+static const char *
+tokenize_host_port (char *address, const char **port)
+{
+ // Unwrap IPv6 addresses in format_host_port_pair() format.
+ char *rbracket = strchr (address, ']');
+ if (*address == '[' && rbracket)
+ {
+ if (rbracket[1] == ':')
+ {
+ *port = rbracket + 2;
+ return *rbracket = 0, address + 1;
+ }
+ if (!rbracket[1])
+ return *rbracket = 0, address + 1;
+ }
+
+ char *colon = strchr (address, ':');
+ if (colon)
+ {
+ *port = colon + 1;
+ return *colon = 0, address;
+ }
+ return address;
+}
+
+// --- To be moved to liberty --------------------------------------------------
+
+// FIXME: in xssl_get_error() we rely on error reasons never being NULL (i.e.,
+// all loaded), which isn't very robust.
+// TODO: check all places where this is used and see if we couldn't gain better
+// information by piecing together some other subset of data from the error
+// stack. Most often, this is used in an error_set() context, which would
+// allow us to allocate memory instead of returning static strings.
+static const char *
+xerr_describe_error (void)
+{
+ unsigned long err = ERR_get_error ();
+ if (!err)
+ return "undefined error";
+
+ const char *reason = ERR_reason_error_string (err);
+ do
+ // Not thread-safe, not a concern right now--need a buffer
+ print_debug ("%s", ERR_error_string (err, NULL));
+ while ((err = ERR_get_error ()));
+
+ if (!reason)
+ return "cannot retrieve error description";
+ return reason;
+}
+
+static struct str
+str_from_cstr (const char *cstr)
+{
+ struct str self;
+ self.alloc = (self.len = strlen (cstr)) + 1;
+ self.str = memcpy (xmalloc (self.alloc), cstr, self.alloc);
+ return self;
+}
+
+static ssize_t
+strv_find (const struct strv *v, const char *s)
+{
+ for (size_t i = 0; i < v->len; i++)
+ if (!strcmp (v->vector[i], s))
+ return i;
+ return -1;
+}
+
+static time_t
+unixtime_msec (long *msec)
+{
+#ifdef _POSIX_TIMERS
+ struct timespec tp;
+ hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
+ *msec = tp.tv_nsec / 1000000;
+#else // ! _POSIX_TIMERS
+ struct timeval tp;
+ hard_assert (gettimeofday (&tp, NULL) != -1);
+ *msec = tp.tv_usec / 1000;
+#endif // ! _POSIX_TIMERS
+ return tp.tv_sec;
+}
+
+// --- Logging -----------------------------------------------------------------
+
+static void
+log_message_syslog (void *user_data, const char *quote, const char *fmt,
+ va_list ap)
+{
+ int prio = (int) (intptr_t) user_data;
+
+ va_list va;
+ va_copy (va, ap);
+ int size = vsnprintf (NULL, 0, fmt, va);
+ va_end (va);
+ if (size < 0)
+ return;
+
+ char buf[size + 1];
+ if (vsnprintf (buf, sizeof buf, fmt, ap) >= 0)
+ syslog (prio, "%s%s", quote, buf);
+}
+
+// --- SOCKS 5/4a --------------------------------------------------------------
+
+// Asynchronous SOCKS connector. Adds more stuff on top of the regular one.
+
+// Note that the `username' is used differently in SOCKS 4a and 5. In the
+// former version, it is the username that you can get ident'ed against.
+// In the latter version, it forms a pair with the password field and doesn't
+// need to be an actual user on your machine.
+
+struct socks_addr
+{
+ enum socks_addr_type
+ {
+ SOCKS_IPV4 = 1, ///< IPv4 address
+ SOCKS_DOMAIN = 3, ///< Domain name to be resolved
+ SOCKS_IPV6 = 4 ///< IPv6 address
+ }
+ type; ///< The type of this address
+ union
+ {
+ uint8_t ipv4[4]; ///< IPv4 address, network octet order
+ char *domain; ///< Domain name
+ uint8_t ipv6[16]; ///< IPv6 address, network octet order
+ }
+ data; ///< The address itself
+};
+
+static void
+socks_addr_free (struct socks_addr *self)
+{
+ if (self->type == SOCKS_DOMAIN)
+ free (self->data.domain);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct socks_target
+{
+ LIST_HEADER (struct socks_target)
+
+ char *address_str; ///< Target address as a string
+ struct socks_addr address; ///< Target address
+ uint16_t port; ///< Target service port
+};
+
+enum socks_protocol
+{
+ SOCKS_5, ///< SOCKS5
+ SOCKS_4A, ///< SOCKS4A
+ SOCKS_MAX ///< End of protocol
+};
+
+static inline const char *
+socks_protocol_to_string (enum socks_protocol self)
+{
+ switch (self)
+ {
+ case SOCKS_5: return "SOCKS5";
+ case SOCKS_4A: return "SOCKS4A";
+ default: return NULL;
+ }
+}
+
+struct socks_connector
+{
+ struct connector *connector; ///< Proxy server iterator (effectively)
+ enum socks_protocol protocol_iter; ///< Protocol iterator
+ struct socks_target *targets_iter; ///< Targets iterator
+
+ // Negotiation:
+
+ struct poller_timer timeout; ///< Timeout timer
+
+ int socket_fd; ///< Current socket file descriptor
+ struct poller_fd socket_event; ///< Socket can be read from/written to
+ struct str read_buffer; ///< Read buffer
+ struct str write_buffer; ///< Write buffer
+
+ bool done; ///< Tunnel succesfully established
+ uint8_t bound_address_len; ///< Length of domain name
+ size_t data_needed; ///< How much data "on_data" needs
+
+ /// Process incoming data if there's enough of it available
+ bool (*on_data) (struct socks_connector *, struct msg_unpacker *);
+
+ // Configuration:
+
+ char *hostname; ///< SOCKS server hostname
+ char *service; ///< SOCKS server service name or port
+
+ char *username; ///< Username for authentication
+ char *password; ///< Password for authentication
+
+ struct socks_target *targets; ///< Targets
+ struct socks_target *targets_tail; ///< Tail of targets
+
+ void *user_data; ///< User data for callbacks
+
+ // Additional results:
+
+ struct socks_addr bound_address; ///< Bound address at the server
+ uint16_t bound_port; ///< Bound port at the server
+
+ // You may destroy the connector object in these two main callbacks:
+
+ /// Connection has been successfully established
+ void (*on_connected) (void *user_data, int socket, const char *hostname);
+ /// Failed to establish a connection to either target
+ void (*on_failure) (void *user_data);
+
+ // Optional:
+
+ /// Connecting to a new address
+ void (*on_connecting) (void *user_data,
+ const char *address, const char *via, const char *version);
+ /// Connecting to the last address has failed
+ void (*on_error) (void *user_data, const char *error);
+};
+
+// I've tried to make the actual protocol handlers as simple as possible
+
+#define SOCKS_FAIL(...) \
+ BLOCK_START \
+ char *error = xstrdup_printf (__VA_ARGS__); \
+ if (self->on_error) \
+ self->on_error (self->user_data, error); \
+ free (error); \
+ return false; \
+ BLOCK_END
+
+#define SOCKS_DATA_CB(name) static bool name \
+ (struct socks_connector *self, struct msg_unpacker *unpacker)
+
+#define SOCKS_GO(name, data_needed_) \
+ self->on_data = name; \
+ self->data_needed = data_needed_; \
+ return true
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+SOCKS_DATA_CB (socks_4a_finish)
+{
+ uint8_t null = 0, status = 0;
+ hard_assert (msg_unpacker_u8 (unpacker, &null));
+ hard_assert (msg_unpacker_u8 (unpacker, &status));
+
+ if (null != 0)
+ SOCKS_FAIL ("protocol error");
+
+ switch (status)
+ {
+ case 90:
+ self->done = true;
+ return false;
+ case 91:
+ SOCKS_FAIL ("request rejected or failed");
+ case 92:
+ SOCKS_FAIL ("%s: %s", "request rejected",
+ "SOCKS server cannot connect to identd on the client");
+ case 93:
+ SOCKS_FAIL ("%s: %s", "request rejected",
+ "identd reports different user-id");
+ default:
+ SOCKS_FAIL ("protocol error");
+ }
+}
+
+static bool
+socks_4a_start (struct socks_connector *self)
+{
+ struct socks_target *target = self->targets_iter;
+ const void *dest_ipv4 = "\x00\x00\x00\x01";
+ const char *dest_domain = NULL;
+
+ char buf[INET6_ADDRSTRLEN];
+ switch (target->address.type)
+ {
+ case SOCKS_IPV4:
+ dest_ipv4 = target->address.data.ipv4;
+ break;
+ case SOCKS_IPV6:
+ // About the best thing we can do, not sure if it works anywhere at all
+ if (!inet_ntop (AF_INET6, &target->address.data.ipv6, buf, sizeof buf))
+ SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno));
+ dest_domain = buf;
+ break;
+ case SOCKS_DOMAIN:
+ dest_domain = target->address.data.domain;
+ }
+
+ struct str *wb = &self->write_buffer;
+ str_pack_u8 (wb, 4); // version
+ str_pack_u8 (wb, 1); // connect
+
+ str_pack_u16 (wb, target->port); // port
+ str_append_data (wb, dest_ipv4, 4); // destination address
+
+ if (self->username)
+ str_append (wb, self->username);
+ str_append_c (wb, '\0');
+
+ if (dest_domain)
+ {
+ str_append (wb, dest_domain);
+ str_append_c (wb, '\0');
+ }
+
+ SOCKS_GO (socks_4a_finish, 8);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+SOCKS_DATA_CB (socks_5_request_port)
+{
+ hard_assert (msg_unpacker_u16 (unpacker, &self->bound_port));
+ self->done = true;
+ return false;
+}
+
+SOCKS_DATA_CB (socks_5_request_ipv4)
+{
+ memcpy (self->bound_address.data.ipv4, unpacker->data, unpacker->len);
+ SOCKS_GO (socks_5_request_port, 2);
+}
+
+SOCKS_DATA_CB (socks_5_request_ipv6)
+{
+ memcpy (self->bound_address.data.ipv6, unpacker->data, unpacker->len);
+ SOCKS_GO (socks_5_request_port, 2);
+}
+
+SOCKS_DATA_CB (socks_5_request_domain_data)
+{
+ self->bound_address.data.domain = xstrndup (unpacker->data, unpacker->len);
+ SOCKS_GO (socks_5_request_port, 2);
+}
+
+SOCKS_DATA_CB (socks_5_request_domain)
+{
+ hard_assert (msg_unpacker_u8 (unpacker, &self->bound_address_len));
+ SOCKS_GO (socks_5_request_domain_data, self->bound_address_len);
+}
+
+SOCKS_DATA_CB (socks_5_request_finish)
+{
+ uint8_t version = 0, status = 0, reserved = 0, type = 0;
+ hard_assert (msg_unpacker_u8 (unpacker, &version));
+ hard_assert (msg_unpacker_u8 (unpacker, &status));
+ hard_assert (msg_unpacker_u8 (unpacker, &reserved));
+ hard_assert (msg_unpacker_u8 (unpacker, &type));
+
+ if (version != 0x05)
+ SOCKS_FAIL ("protocol error");
+
+ switch (status)
+ {
+ case 0x00:
+ break;
+ case 0x01: SOCKS_FAIL ("general SOCKS server failure");
+ case 0x02: SOCKS_FAIL ("connection not allowed by ruleset");
+ case 0x03: SOCKS_FAIL ("network unreachable");
+ case 0x04: SOCKS_FAIL ("host unreachable");
+ case 0x05: SOCKS_FAIL ("connection refused");
+ case 0x06: SOCKS_FAIL ("TTL expired");
+ case 0x07: SOCKS_FAIL ("command not supported");
+ case 0x08: SOCKS_FAIL ("address type not supported");
+ default: SOCKS_FAIL ("protocol error");
+ }
+
+ switch ((self->bound_address.type = type))
+ {
+ case SOCKS_IPV4:
+ SOCKS_GO (socks_5_request_ipv4, sizeof self->bound_address.data.ipv4);
+ case SOCKS_IPV6:
+ SOCKS_GO (socks_5_request_ipv6, sizeof self->bound_address.data.ipv6);
+ case SOCKS_DOMAIN:
+ SOCKS_GO (socks_5_request_domain, 1);
+ default:
+ SOCKS_FAIL ("protocol error");
+ }
+}
+
+static bool
+socks_5_request_start (struct socks_connector *self)
+{
+ struct socks_target *target = self->targets_iter;
+ struct str *wb = &self->write_buffer;
+ str_pack_u8 (wb, 0x05); // version
+ str_pack_u8 (wb, 0x01); // connect
+ str_pack_u8 (wb, 0x00); // reserved
+ str_pack_u8 (wb, target->address.type);
+
+ switch (target->address.type)
+ {
+ case SOCKS_IPV4:
+ str_append_data (wb,
+ target->address.data.ipv4, sizeof target->address.data.ipv4);
+ break;
+ case SOCKS_DOMAIN:
+ {
+ size_t dlen = strlen (target->address.data.domain);
+ if (dlen > 255)
+ dlen = 255;
+
+ str_pack_u8 (wb, dlen);
+ str_append_data (wb, target->address.data.domain, dlen);
+ break;
+ }
+ case SOCKS_IPV6:
+ str_append_data (wb,
+ target->address.data.ipv6, sizeof target->address.data.ipv6);
+ break;
+ }
+ str_pack_u16 (wb, target->port);
+
+ SOCKS_GO (socks_5_request_finish, 4);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+SOCKS_DATA_CB (socks_5_userpass_finish)
+{
+ uint8_t version = 0, status = 0;
+ hard_assert (msg_unpacker_u8 (unpacker, &version));
+ hard_assert (msg_unpacker_u8 (unpacker, &status));
+
+ if (version != 0x01)
+ SOCKS_FAIL ("protocol error");
+ if (status != 0x00)
+ SOCKS_FAIL ("authentication failure");
+
+ return socks_5_request_start (self);
+}
+
+static bool
+socks_5_userpass_start (struct socks_connector *self)
+{
+ size_t ulen = strlen (self->username);
+ if (ulen > 255)
+ ulen = 255;
+
+ size_t plen = strlen (self->password);
+ if (plen > 255)
+ plen = 255;
+
+ struct str *wb = &self->write_buffer;
+ str_pack_u8 (wb, 0x01); // version
+ str_pack_u8 (wb, ulen); // username length
+ str_append_data (wb, self->username, ulen);
+ str_pack_u8 (wb, plen); // password length
+ str_append_data (wb, self->password, plen);
+
+ SOCKS_GO (socks_5_userpass_finish, 2);
+}
+
+SOCKS_DATA_CB (socks_5_auth_finish)
+{
+ uint8_t version = 0, method = 0;
+ hard_assert (msg_unpacker_u8 (unpacker, &version));
+ hard_assert (msg_unpacker_u8 (unpacker, &method));
+
+ if (version != 0x05)
+ SOCKS_FAIL ("protocol error");
+
+ bool can_auth = self->username && self->password;
+
+ switch (method)
+ {
+ case 0x02:
+ if (!can_auth)
+ SOCKS_FAIL ("protocol error");
+
+ return socks_5_userpass_start (self);
+ case 0x00:
+ return socks_5_request_start (self);
+ case 0xFF:
+ SOCKS_FAIL ("no acceptable authentication methods");
+ default:
+ SOCKS_FAIL ("protocol error");
+ }
+}
+
+static bool
+socks_5_auth_start (struct socks_connector *self)
+{
+ bool can_auth = self->username && self->password;
+
+ struct str *wb = &self->write_buffer;
+ str_pack_u8 (wb, 0x05); // version
+ str_pack_u8 (wb, 1 + can_auth); // number of authentication methods
+ str_pack_u8 (wb, 0x00); // no authentication required
+ if (can_auth)
+ str_pack_u8 (wb, 0x02); // username/password
+
+ SOCKS_GO (socks_5_auth_finish, 2);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void socks_connector_start (struct socks_connector *self);
+
+static void
+socks_connector_destroy_connector (struct socks_connector *self)
+{
+ if (self->connector)
+ {
+ connector_free (self->connector);
+ free (self->connector);
+ self->connector = NULL;
+ }
+}
+
+static void
+socks_connector_cancel_events (struct socks_connector *self)
+{
+ // Before calling the final callbacks, we should cancel events that
+ // could potentially fire; caller should destroy us immediately, though
+ poller_fd_reset (&self->socket_event);
+ poller_timer_reset (&self->timeout);
+}
+
+static void
+socks_connector_fail (struct socks_connector *self)
+{
+ socks_connector_cancel_events (self);
+ self->on_failure (self->user_data);
+}
+
+static bool
+socks_connector_step_iterators (struct socks_connector *self)
+{
+ // At the lowest level we iterate over all addresses for the SOCKS server
+ // and just try to connect; this is done automatically by the connector
+
+ // Then we iterate over available protocols
+ if (++self->protocol_iter != SOCKS_MAX)
+ return true;
+
+ // At the highest level we iterate over possible targets
+ self->protocol_iter = 0;
+ if (self->targets_iter && (self->targets_iter = self->targets_iter->next))
+ return true;
+
+ return false;
+}
+
+static void
+socks_connector_step (struct socks_connector *self)
+{
+ if (self->socket_fd != -1)
+ {
+ poller_fd_reset (&self->socket_event);
+ xclose (self->socket_fd);
+ self->socket_fd = -1;
+ }
+
+ socks_connector_destroy_connector (self);
+ if (socks_connector_step_iterators (self))
+ socks_connector_start (self);
+ else
+ socks_connector_fail (self);
+}
+
+static void
+socks_connector_on_timeout (struct socks_connector *self)
+{
+ if (self->on_error)
+ self->on_error (self->user_data, "timeout");
+
+ socks_connector_destroy_connector (self);
+ socks_connector_fail (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+socks_connector_on_connected
+ (void *user_data, int socket_fd, const char *hostname)
+{
+ set_blocking (socket_fd, false);
+ (void) hostname;
+
+ struct socks_connector *self = user_data;
+ self->socket_fd = socket_fd;
+ self->socket_event.fd = socket_fd;
+ poller_fd_set (&self->socket_event, POLLIN | POLLOUT);
+ str_reset (&self->read_buffer);
+ str_reset (&self->write_buffer);
+
+ if (!(self->protocol_iter == SOCKS_5 && socks_5_auth_start (self))
+ && !(self->protocol_iter == SOCKS_4A && socks_4a_start (self)))
+ socks_connector_fail (self);
+}
+
+static void
+socks_connector_on_failure (void *user_data)
+{
+ struct socks_connector *self = user_data;
+ // TODO: skip SOCKS server on connection failure
+ socks_connector_step (self);
+}
+
+static void
+socks_connector_on_connecting (void *user_data, const char *via)
+{
+ struct socks_connector *self = user_data;
+ if (!self->on_connecting)
+ return;
+
+ struct socks_target *target = self->targets_iter;
+ char *port = xstrdup_printf ("%u", target->port);
+ char *address = format_host_port_pair (target->address_str, port);
+ free (port);
+ self->on_connecting (self->user_data, address, via,
+ socks_protocol_to_string (self->protocol_iter));
+ free (address);
+}
+
+static void
+socks_connector_on_error (void *user_data, const char *error)
+{
+ struct socks_connector *self = user_data;
+ // TODO: skip protocol on protocol failure
+ if (self->on_error)
+ self->on_error (self->user_data, error);
+}
+
+static void
+socks_connector_start (struct socks_connector *self)
+{
+ hard_assert (!self->connector);
+
+ struct connector *connector =
+ self->connector = xcalloc (1, sizeof *connector);
+ connector_init (connector, self->socket_event.poller);
+
+ connector->user_data = self;
+ connector->on_connected = socks_connector_on_connected;
+ connector->on_connecting = socks_connector_on_connecting;
+ connector->on_error = socks_connector_on_error;
+ connector->on_failure = socks_connector_on_failure;
+
+ connector_add_target (connector, self->hostname, self->service);
+ poller_timer_set (&self->timeout, 60 * 1000);
+ self->done = false;
+
+ self->bound_port = 0;
+ socks_addr_free (&self->bound_address);
+ memset (&self->bound_address, 0, sizeof self->bound_address);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+socks_try_fill_read_buffer (struct socks_connector *self, size_t n)
+{
+ ssize_t remains = (ssize_t) n - (ssize_t) self->read_buffer.len;
+ if (remains <= 0)
+ return true;
+
+ ssize_t received;
+ str_reserve (&self->read_buffer, remains);
+ do
+ received = recv (self->socket_fd,
+ self->read_buffer.str + self->read_buffer.len, remains, 0);
+ while ((received == -1) && errno == EINTR);
+
+ if (received == 0)
+ SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF");
+ if (received == -1 && errno != EAGAIN)
+ SOCKS_FAIL ("%s: %s", "recv", strerror (errno));
+ if (received > 0)
+ self->read_buffer.len += received;
+ return true;
+}
+
+static bool
+socks_call_on_data (struct socks_connector *self)
+{
+ size_t to_consume = self->data_needed;
+ if (!socks_try_fill_read_buffer (self, to_consume))
+ return false;
+ if (self->read_buffer.len < to_consume)
+ return true;
+
+ struct msg_unpacker unpacker =
+ msg_unpacker_make (self->read_buffer.str, self->read_buffer.len);
+ bool result = self->on_data (self, &unpacker);
+ str_remove_slice (&self->read_buffer, 0, to_consume);
+ return result;
+}
+
+static bool
+socks_try_flush_write_buffer (struct socks_connector *self)
+{
+ struct str *wb = &self->write_buffer;
+ ssize_t n_written;
+
+ while (wb->len)
+ {
+ n_written = send (self->socket_fd, wb->str, wb->len, 0);
+ if (n_written >= 0)
+ {
+ str_remove_slice (wb, 0, n_written);
+ continue;
+ }
+
+ if (errno == EAGAIN)
+ break;
+ if (errno == EINTR)
+ continue;
+
+ SOCKS_FAIL ("%s: %s", "send", strerror (errno));
+ }
+ return true;
+}
+
+static void
+socks_connector_on_ready
+ (const struct pollfd *pfd, struct socks_connector *self)
+{
+ (void) pfd;
+
+ if (socks_call_on_data (self) && socks_try_flush_write_buffer (self))
+ {
+ poller_fd_set (&self->socket_event,
+ self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
+ }
+ else if (self->done)
+ {
+ socks_connector_cancel_events (self);
+
+ int fd = self->socket_fd;
+ self->socket_fd = -1;
+
+ struct socks_target *target = self->targets_iter;
+ set_blocking (fd, true);
+ self->on_connected (self->user_data, fd, target->address_str);
+ }
+ else
+ // We've failed this target, let's try to move on
+ socks_connector_step (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+socks_connector_init (struct socks_connector *self, struct poller *poller)
+{
+ memset (self, 0, sizeof *self);
+
+ self->socket_event = poller_fd_make (poller, (self->socket_fd = -1));
+ self->socket_event.dispatcher = (poller_fd_fn) socks_connector_on_ready;
+ self->socket_event.user_data = self;
+
+ self->timeout = poller_timer_make (poller);
+ self->timeout.dispatcher = (poller_timer_fn) socks_connector_on_timeout;
+ self->timeout.user_data = self;
+
+ self->read_buffer = str_make ();
+ self->write_buffer = str_make ();
+}
+
+static void
+socks_connector_free (struct socks_connector *self)
+{
+ socks_connector_destroy_connector (self);
+ socks_connector_cancel_events (self);
+
+ if (self->socket_fd != -1)
+ xclose (self->socket_fd);
+
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+
+ free (self->hostname);
+ free (self->service);
+ free (self->username);
+ free (self->password);
+
+ LIST_FOR_EACH (struct socks_target, iter, self->targets)
+ {
+ socks_addr_free (&iter->address);
+ free (iter->address_str);
+ free (iter);
+ }
+
+ socks_addr_free (&self->bound_address);
+}
+
+static bool
+socks_connector_add_target (struct socks_connector *self,
+ const char *host, const char *service, struct error **e)
+{
+ unsigned long port;
+ const struct servent *serv;
+ if ((serv = getservbyname (service, "tcp")))
+ port = (uint16_t) ntohs (serv->s_port);
+ else if (!xstrtoul (&port, service, 10) || !port || port > UINT16_MAX)
+ {
+ error_set (e, "invalid port number");
+ return false;
+ }
+
+ struct socks_target *target = xcalloc (1, sizeof *target);
+ if (inet_pton (AF_INET, host, &target->address.data.ipv4) == 1)
+ target->address.type = SOCKS_IPV4;
+ else if (inet_pton (AF_INET6, host, &target->address.data.ipv6) == 1)
+ target->address.type = SOCKS_IPV6;
+ else
+ {
+ target->address.type = SOCKS_DOMAIN;
+ target->address.data.domain = xstrdup (host);
+ }
+
+ target->port = port;
+ target->address_str = xstrdup (host);
+ LIST_APPEND_WITH_TAIL (self->targets, self->targets_tail, target);
+ return true;
+}
+
+static void
+socks_connector_run (struct socks_connector *self,
+ const char *host, const char *service,
+ const char *username, const char *password)
+{
+ hard_assert (self->targets);
+ hard_assert (host && service);
+
+ self->hostname = xstrdup (host);
+ self->service = xstrdup (service);
+
+ if (username) self->username = xstrdup (username);
+ if (password) self->password = xstrdup (password);
+
+ self->targets_iter = self->targets;
+ self->protocol_iter = 0;
+ // XXX: this can fail immediately from an error creating the connector
+ socks_connector_start (self);
+}
+
+// --- CTCP decoding -----------------------------------------------------------
+
+#define CTCP_M_QUOTE '\020'
+#define CTCP_X_DELIM '\001'
+#define CTCP_X_QUOTE '\\'
+
+struct ctcp_chunk
+{
+ LIST_HEADER (struct ctcp_chunk)
+
+ bool is_extended; ///< Is this a tagged extended message?
+ bool is_partial; ///< Unterminated extended message
+ struct str tag; ///< The tag, if any
+ struct str text; ///< Message contents
+};
+
+static struct ctcp_chunk *
+ctcp_chunk_new (void)
+{
+ struct ctcp_chunk *self = xcalloc (1, sizeof *self);
+ self->tag = str_make ();
+ self->text = str_make ();
+ return self;
+}
+
+static void
+ctcp_chunk_destroy (struct ctcp_chunk *self)
+{
+ str_free (&self->tag);
+ str_free (&self->text);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+ctcp_low_level_decode (const char *message, struct str *output)
+{
+ bool escape = false;
+ for (const char *p = message; *p; p++)
+ {
+ if (escape)
+ {
+ switch (*p)
+ {
+ case '0': str_append_c (output, '\0'); break;
+ case 'r': str_append_c (output, '\r'); break;
+ case 'n': str_append_c (output, '\n'); break;
+ default: str_append_c (output, *p);
+ }
+ escape = false;
+ }
+ else if (*p == CTCP_M_QUOTE)
+ escape = true;
+ else
+ str_append_c (output, *p);
+ }
+}
+
+static void
+ctcp_intra_decode (const char *chunk, size_t len, struct str *output)
+{
+ bool escape = false;
+ for (size_t i = 0; i < len; i++)
+ {
+ char c = chunk[i];
+ if (escape)
+ {
+ if (c == 'a')
+ str_append_c (output, CTCP_X_DELIM);
+ else
+ str_append_c (output, c);
+ escape = false;
+ }
+ else if (c == CTCP_X_QUOTE)
+ escape = true;
+ else
+ str_append_c (output, c);
+ }
+}
+
+// According to the original CTCP specification we should use
+// ctcp_intra_decode() on all parts, however no one seems to use that
+// and it breaks normal text with backslashes
+#ifndef SUPPORT_CTCP_X_QUOTES
+#define ctcp_intra_decode(s, len, output) str_append_data (output, s, len)
+#endif
+
+static void
+ctcp_parse_tagged (const char *chunk, size_t len, struct ctcp_chunk *output)
+{
+ // We may search for the space before doing the higher level decoding,
+ // as it doesn't concern space characters at all
+ size_t tag_end = len;
+ for (size_t i = 0; i < len; i++)
+ if (chunk[i] == ' ')
+ {
+ tag_end = i;
+ break;
+ }
+
+ output->is_extended = true;
+ ctcp_intra_decode (chunk, tag_end, &output->tag);
+ if (tag_end++ != len)
+ ctcp_intra_decode (chunk + tag_end, len - tag_end, &output->text);
+}
+
+static struct ctcp_chunk *
+ctcp_parse (const char *message)
+{
+ struct str m = str_make ();
+ ctcp_low_level_decode (message, &m);
+
+ struct ctcp_chunk *result = NULL, *result_tail = NULL;
+
+ size_t start = 0;
+ bool in_ctcp = false;
+ for (size_t i = 0; i < m.len; i++)
+ {
+ char c = m.str[i];
+ if (c != CTCP_X_DELIM)
+ continue;
+
+ // Remember the current state
+ size_t my_start = start;
+ bool my_is_ctcp = in_ctcp;
+
+ start = i + 1;
+ in_ctcp = !in_ctcp;
+
+ // Skip empty chunks
+ if (my_start == i)
+ continue;
+
+ struct ctcp_chunk *chunk = ctcp_chunk_new ();
+ if (my_is_ctcp)
+ ctcp_parse_tagged (m.str + my_start, i - my_start, chunk);
+ else
+ ctcp_intra_decode (m.str + my_start, i - my_start, &chunk->text);
+ LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
+ }
+
+ // Finish the last part. Unended tagged chunks are marked as such.
+ if (start != m.len)
+ {
+ struct ctcp_chunk *chunk = ctcp_chunk_new ();
+ if (in_ctcp)
+ {
+ ctcp_parse_tagged (m.str + start, m.len - start, chunk);
+ chunk->is_partial = true;
+ }
+ else
+ ctcp_intra_decode (m.str + start, m.len - start, &chunk->text);
+ LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
+ }
+
+ str_free (&m);
+ return result;
+}
+
+static void
+ctcp_destroy (struct ctcp_chunk *list)
+{
+ LIST_FOR_EACH (struct ctcp_chunk, iter, list)
+ ctcp_chunk_destroy (iter);
+}
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..6f3911f
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,15 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define PROGRAM_VERSION "${project_version}"
+
+// We use the XDG Base Directory Specification, but may be installed anywhere.
+#define PROJECT_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}"
+
+#cmakedefine HAVE_READLINE
+#cmakedefine HAVE_EDITLINE
+#cmakedefine HAVE_LUA
+
+#cmakedefine01 ICONV_ACCEPTS_TRANSLIT
+
+#endif // ! CONFIG_H
diff --git a/liberty b/liberty
new file mode 160000
+Subproject 34460ca715b295cc73c3f2bba4156c7f072ce12
diff --git a/plugins/xB/calc b/plugins/xB/calc
new file mode 100755
index 0000000..7d2d660
--- /dev/null
+++ b/plugins/xB/calc
@@ -0,0 +1,241 @@
+#!/usr/bin/env guile
+
+ xB calc plugin, basic Scheme evaluator
+
+ Copyright 2016 Přemysl Eric Janouch
+ See the file LICENSE for licensing information.
+
+!#
+
+(import (rnrs (6)))
+(use-modules ((rnrs) :version (6)))
+
+; --- Message parsing ----------------------------------------------------------
+
+(define-record-type message (fields prefix command params))
+(define (parse-message line)
+ (let f ([parts '()] [chars (string->list line)])
+ (define (take-word w chars)
+ (if (or (null? chars) (eqv? (car chars) #\x20))
+ (f (cons (list->string (reverse w)) parts)
+ (if (null? chars) chars (cdr chars)))
+ (take-word (cons (car chars) w) (cdr chars))))
+ (if (null? chars)
+ (let ([data (reverse parts)])
+ (when (< (length data) 2)
+ (error 'parse-message "invalid message"))
+ (make-message (car data) (cadr data) (cddr data)))
+ (if (null? parts)
+ (if (eqv? (car chars) #\:)
+ (take-word '() (cdr chars))
+ (f (cons #f parts) chars))
+ (if (eqv? (car chars) #\:)
+ (f (cons (list->string (cdr chars)) parts) '())
+ (take-word '() chars))))))
+
+; --- Utilities ----------------------------------------------------------------
+
+(define (display-exception e port)
+ (define (puts . x)
+ (for-all (lambda (a) (display a port)) x)
+ (newline port))
+
+ (define (record-fields rec)
+ (let* ([rtd (record-rtd rec)]
+ [v (record-type-field-names rtd)]
+ [len (vector-length v)])
+ (map (lambda (k i) (cons k ((record-accessor rtd i) rec)))
+ (vector->list v)
+ (let c ([i len] [ls '()])
+ (if (= i 0) ls (c (- i 1) (cons (- i 1) ls)))))))
+
+ (puts "Caught " (record-type-name (record-rtd e)))
+ (for-all
+ (lambda (subtype)
+ (puts " " (record-type-name (record-rtd subtype)))
+ (for-all
+ (lambda (field) (puts " " (car field) ": " (cdr field)))
+ (record-fields subtype)))
+ (simple-conditions e)))
+
+; XXX - we have to work around Guile's lack of proper eol-style support
+(define xc (make-transcoder (latin-1-codec) 'lf 'replace))
+(define irc-input-port (transcoded-port (standard-input-port) xc))
+(define irc-output-port (transcoded-port (standard-output-port) xc))
+
+(define (send . message)
+ (for-all (lambda (x) (display x irc-output-port)) message)
+ (display #\return irc-output-port)
+ (newline irc-output-port)
+ (flush-output-port irc-output-port))
+
+(define (get-line-crlf port)
+ (define line (get-line port))
+ (if (eof-object? line) line
+ (let ([len (string-length line)])
+ (if (and (> len 0) (eqv? (string-ref line (- len 1)) #\return))
+ (substring line 0 (- len 1)) line))))
+
+(define (get-config name)
+ (send "XB get_config :" name)
+ (car (message-params (parse-message (get-line-crlf irc-input-port)))))
+
+(define (extract-nick prefix)
+ (do ([i 0 (+ i 1)] [len (string-length prefix)])
+ ([or (= i len) (char=? #\! (string-ref prefix i))]
+ [substring prefix 0 i])))
+
+(define (string-after s start)
+ (let ([s-len (string-length s)] [with-len (string-length start)])
+ (and (>= s-len with-len)
+ (string=? (substring s 0 with-len) start)
+ (substring s with-len s-len))))
+
+; --- Calculator ---------------------------------------------------------------
+
+; Evaluator derived from the example in The Scheme Programming Language.
+;
+; Even though EVAL with a carefully crafted environment would also do a good
+; job at sandboxing, it would probably be impossible to limit execution time...
+
+(define (env-new formals actuals env)
+ (cond [(null? formals) env]
+ [(symbol? formals) (cons (cons formals actuals) env)]
+ [else (cons (cons (car formals) (car actuals))
+ (env-new (cdr formals) (cdr actuals) env))]))
+(define (env-lookup var env) (cdr (assq var env)))
+(define (env-assign var val env) (set-cdr! (assq var env) val))
+
+(define (check-reductions r)
+ (if (= (car r) 0)
+ (error 'check-reductions "reduction limit exceeded")
+ (set-car! r (- (car r) 1))))
+
+; TODO - think about implementing more syntactical constructs,
+; however there's not much point in having anything else in a calculator...
+(define (exec expr r env)
+ (check-reductions r)
+ (cond [(symbol? expr) (env-lookup expr env)]
+ [(pair? expr)
+ (case (car expr)
+ [(quote) (cadr expr)]
+ [(lambda) (lambda vals
+ (let ([env (env-new (cadr expr) vals env)])
+ (let loop ([exprs (cddr expr)])
+ (if (null? (cdr exprs))
+ (exec (car exprs) r env)
+ (begin (exec (car exprs) r env)
+ (loop (cdr exprs)))))))]
+ [(if) (if (exec (cadr expr) r env)
+ (exec (caddr expr) r env)
+ (exec (cadddr expr) r env))]
+ [(set!) (env-assign (cadr expr) (exec (caddr expr) r env) env)]
+ [else (apply (exec (car expr) r env)
+ (map (lambda (x) (exec x r env)) (cdr expr)))])]
+ [else expr]))
+
+(define-syntax forward
+ (syntax-rules ()
+ [(_) '()]
+ [(_ a b ...) (cons (cons (quote a) a) (forward b ...))]))
+
+; ...which can't prevent me from simply importing most of the standard library
+(define base-library
+ (forward
+ ; Equivalence, procedure predicate, booleans
+ eqv? eq? equal? procedure? boolean? boolean=? not
+ ; numbers, numerical input and output
+ number? complex? real? rational? integer? exact? inexact? exact inexact
+ real-valued? rational-valued? integer-valued? number->string string->number
+ ; Arithmetic
+ = < > <= >= zero? positive? negative? odd? even? finite? infinite? nan?
+ min max + * - / abs div-and-mod div mod div0-and-mod0 div0 mod0
+ gcd lcm numerator denominator floor ceiling truncate round
+ rationalize exp log sin cos tan asin acos atan sqrt expt
+ make-rectangular make-polar real-part imag-part magnitude angle
+ ; Pairs and lists
+ map for-each cons car cdr caar cadr cdar cddr
+ caaar caadr cadar caddr cdaar cdadr cddar cdddr
+ caaaar caaadr caadar caaddr cadaar cadadr caddar cadddr
+ cdaaar cdaadr cdadar cdaddr cddaar cddadr cdddar cddddr
+ pair? null? list? list length append reverse list-tail list-ref
+ ; Symbols
+ symbol? symbol=? symbol->string string->symbol
+ ; Characters
+ char? char=? char<? char>? char<=? char>=? char->integer integer->char
+ ; Strings; XXX - omitted make-string - can cause OOM
+ string? string=? string<? string>? string<=? string>=?
+ string string-length string-ref substring
+ string-append string->list list->string string-for-each string-copy
+ ; Vectors; XXX - omitted make-vector - can cause OOM
+ vector? vector vector-length vector-ref vector-set!
+ vector->list list->vector vector-fill! vector-map vector-for-each
+ ; Control features
+ apply call/cc values call-with-values dynamic-wind))
+(define extended-library
+ (forward
+ char-upcase char-downcase char-titlecase char-foldcase
+ char-ci=? char-ci<? char-ci>? char-ci<=? char-ci>=?
+ char-alphabetic? char-numeric? char-whitespace?
+ char-upper-case? char-lower-case? char-title-case?
+ string-upcase string-downcase string-titlecase string-foldcase
+ string-ci=? string-ci<? string-ci>? string-ci<=? string-ci>=?
+ find for-all exists filter partition fold-left fold-right
+ remp remove remv remq memp member memv memq assp assoc assv assq cons*
+ list-sort vector-sort vector-sort!
+ bitwise-not bitwise-and bitwise-ior bitwise-xor bitwise-if
+ bitwise-bit-count bitwise-length bitwise-first-bit-set bitwise-bit-set?
+ bitwise-copy-bit bitwise-bit-field bitwise-copy-bit-field
+ bitwise-arithmetic-shift bitwise-rotate-bit-field bitwise-reverse-bit-field
+ bitwise-arithmetic-shift-left bitwise-arithmetic-shift-right
+ set-car! set-cdr! string-set! string-fill!))
+(define (interpret expr)
+ (exec expr '(2000) (append base-library extended-library)))
+
+; We could show something a bit nicer but it would be quite Guile-specific
+(define (error-string e)
+ (map (lambda (x) (string-append " " (symbol->string x)))
+ (filter (lambda (x) (not (member x '(&who &message &irritants &guile))))
+ (map (lambda (x) (record-type-name (record-rtd x)))
+ (simple-conditions e)))))
+
+(define (calc input respond)
+ (define (stringify x)
+ (call-with-string-output-port (lambda (port) (write x port))))
+ (guard (e [else (display-exception e (current-error-port))
+ (apply respond "caught" (error-string e))])
+ (let* ([input (open-string-input-port input)]
+ [data (let loop ()
+ (define datum (get-datum input))
+ (if (eof-object? datum) '() (cons datum (loop))))])
+ (call-with-values
+ (lambda () (interpret (list (append '(lambda ()) data))))
+ (lambda message
+ (for-all (lambda (x) (respond (stringify x))) message))))))
+
+; --- Main loop ----------------------------------------------------------------
+
+(define prefix (get-config "prefix"))
+(send "XB register")
+
+(define (process msg)
+ (when (string-ci=? (message-command msg) "PRIVMSG")
+ (let* ([nick (extract-nick (message-prefix msg))]
+ [target (car (message-params msg))]
+ [response-begin
+ (apply string-append "PRIVMSG "
+ (if (memv (string-ref target 0) (string->list "#&!+"))
+ `(,target " :" ,nick ": ") `(,nick " :")))]
+ [respond (lambda args (apply send response-begin args))]
+ [text (cadr (message-params msg))]
+ [input (or (string-after text (string-append prefix "calc "))
+ (string-after text (string-append prefix "= ")))])
+ (when input (calc input respond)))))
+
+(let main-loop ()
+ (define line (get-line-crlf irc-input-port))
+ (unless (eof-object? line)
+ (guard (e [else (display-exception e (current-error-port))])
+ (unless (string=? "" line)
+ (process (parse-message line))))
+ (main-loop)))
diff --git a/plugins/xB/coin b/plugins/xB/coin
new file mode 100755
index 0000000..770cb32
--- /dev/null
+++ b/plugins/xB/coin
@@ -0,0 +1,128 @@
+#!/usr/bin/env tclsh
+#
+# xB coin plugin, random number-based utilities
+#
+# Copyright 2012, 2014 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+# This is a terrible excuse for a programming language and I feel dirty.
+
+proc parse {line} {
+ global msg
+ unset -nocomplain msg
+
+ if [regexp {^:([^ ]*) *(.*)} $line -> prefix rest] {
+ set msg(prefix) $prefix
+ set line $rest
+ }
+ if [regexp {^([^ ]*) *(.*)} $line -> command rest] {
+ set msg(command) $command
+ set line $rest
+ }
+ while {1} {
+ set line [string trimleft $line " "]
+ set i [string first " " $line]
+ if {$i == -1} { set i [string length $line] }
+ if {$i == 0} { break }
+
+ if {[string index $line 0] == ":"} {
+ lappend msg(param) [string range $line 1 end]
+ break
+ }
+ lappend msg(param) [string range $line 0 [expr $i - 1]]
+ set line [string range $line $i end]
+ }
+}
+
+proc get_config {key} {
+ global msg
+ puts "XB get_config :$key"
+ gets stdin line
+ parse $line
+ return [lindex $msg(param) 0]
+}
+
+proc pmrespond {text} {
+ global ctx
+ global ctx_quote
+ puts "PRIVMSG $ctx :$ctx_quote$text"
+}
+
+fconfigure stdin -translation crlf -encoding iso8859-1
+fconfigure stdout -translation crlf -encoding iso8859-1
+
+set prefix [get_config prefix]
+puts "XB register"
+
+set eightball [list \
+ "It is certain" \
+ "It is decidedly so" \
+ "Without a doubt" \
+ "Yes - definitely" \
+ "You may rely on it" \
+ "As I see it, yes" \
+ "Most likely" \
+ "Outlook good" \
+ "Yes" \
+ "Signs point to yes" \
+ "Reply hazy, try again" \
+ "Ask again later" \
+ "Better not tell you now" \
+ "Cannot predict now" \
+ "Concentrate and ask again" \
+ "Don't count on it" \
+ "My reply is no" \
+ "My sources say no" \
+ "Outlook not so good" \
+ "Very doubtful"]
+
+while {[gets stdin line] != -1} {
+ parse $line
+
+ if {! [info exists msg(prefix)] || ! [info exists msg(command)]
+ || $msg(command) != "PRIVMSG" || ! [info exists msg(param)]
+ || [llength $msg(param)] < 2} { continue }
+
+ regexp {^[^!]*} $msg(prefix) ctx
+ if [regexp {^[#&+!]} [lindex $msg(param) 0]] {
+ set ctx_quote "$ctx: "
+ set ctx [lindex $msg(param) 0]
+ } else { set ctx_quote "" }
+
+ set input [lindex $msg(param) 1]
+ set first_chars [string range $input 0 \
+ [expr [string length $prefix] - 1]]
+ if {$first_chars != $prefix} { continue }
+ set input [string range $input [string length $prefix] end]
+
+ if {$input == "coin"} {
+ if {rand() < 0.5} {
+ pmrespond "Heads."
+ } else {
+ pmrespond "Tails."
+ }
+ } elseif {[regexp {^dice( +|$)(.*)} $input -> _ args]} {
+ if {! [string is integer -strict $args] || $args <= 0} {
+ pmrespond "Invalid or missing number."
+ } else {
+ pmrespond [expr {int($args * rand()) + 1}]
+ }
+ } elseif {[regexp {^(choose|\?)( +|$)(.*)} $input -> _ _ args]} {
+ if {$args == ""} {
+ pmrespond "Nothing to choose from."
+ } else {
+ set c [split $args ",|"]
+ pmrespond [string trim [lindex $c \
+ [expr {int([llength $c] * rand())}]]]
+ }
+ } elseif {[regexp {^eightball( +|$)(.*)} $input -> _ args]} {
+ if {$args == ""} {
+ pmrespond "You should, you know, ask something."
+ } else {
+ pmrespond [lindex $eightball \
+ [expr {int([llength $eightball] * rand())}]].
+ }
+ }
+}
+
diff --git a/plugins/xB/eval b/plugins/xB/eval
new file mode 100755
index 0000000..48ea28d
--- /dev/null
+++ b/plugins/xB/eval
@@ -0,0 +1,312 @@
+#!/usr/bin/awk -f
+#
+# xB eval plugin, LISP-like expression evaluator
+#
+# Copyright 2013, 2014 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+BEGIN \
+{
+ RS = "\r"
+ ORS = "\r\n"
+ IGNORECASE = 1
+ srand()
+
+ prefix = get_config("prefix")
+
+ print "XB register"
+ fflush("")
+
+ # All functions have to be in this particular array
+ min_args["int"] = 1
+ min_args["+"] = 1
+ min_args["-"] = 1
+ min_args["*"] = 1
+ min_args["/"] = 1
+ min_args["%"] = 1
+ min_args["^"] = 1
+ min_args["**"] = 1
+ min_args["exp"] = 1
+ min_args["sin"] = 1
+ min_args["cos"] = 1
+ min_args["atan2"] = 2
+ min_args["log"] = 1
+ min_args["rand"] = 0
+ min_args["sqrt"] = 1
+
+ min_args["pi"] = 0
+ min_args["e"] = 0
+
+ min_args["min"] = 1
+ min_args["max"] = 1
+
+ # Whereas here their presence is only optional
+ max_args["int"] = 1
+ max_args["sin"] = 1
+ max_args["cos"] = 1
+ max_args["atan2"] = 2
+ max_args["log"] = 1
+ max_args["rand"] = 0
+ max_args["sqrt"] = 1
+
+ max_args["pi"] = 0
+ max_args["e"] = 0
+}
+
+{
+ parse($0)
+}
+
+msg_command == "PRIVMSG" \
+{
+ # Context = either channel or user nickname
+ match(msg_prefix, /^[^!]+/)
+ ctx = substr(msg_prefix, RSTART, RLENGTH)
+ if (msg_param[0] ~ /^[#&!+]/)
+ {
+ ctx_quote = ctx ": "
+ ctx = msg_param[0]
+ }
+ else
+ ctx_quote = ""
+
+
+ if (substr(msg_param[1], 1, length(prefix)) == prefix)
+ {
+ keyword = "eval"
+ text = substr(msg_param[1], 1 + length(prefix))
+ if (match(text, "^" keyword "([^A-Za-z0-9].*|$)"))
+ process_request(substr(text, 1 + length(keyword)))
+ }
+}
+
+{
+ fflush("")
+}
+
+function pmrespond (text)
+{
+ print "PRIVMSG " ctx " :" ctx_quote text
+}
+
+function process_request (input, res, x)
+{
+ delete funs
+ delete accumulator
+ delete n_args
+
+ res = ""
+ fun_top = 0
+ funs[0] = ""
+ accumulator[0] = 0
+ n_args[0] = 0
+
+ if (match(input, "^[ \t]*"))
+ input = substr(input, RLENGTH + 1)
+ if (input == "")
+ res = "expression missing"
+
+ while (res == "" && input != "") {
+ if (match(input, "^-?[0-9]+\\.?[0-9]*")) {
+ x = substr(input, RSTART, RLENGTH)
+ input = substr(input, RLENGTH + 1)
+
+ match(input, "^ *")
+ input = substr(input, RLENGTH + 1)
+
+ res = process_argument(x)
+ } else if (match(input, "^[(]([^ ()]+)")) {
+ x = substr(input, RSTART + 1, RLENGTH - 1)
+ input = substr(input, RLENGTH + 1)
+
+ match(input, "^ *")
+ input = substr(input, RLENGTH + 1)
+
+ if (!(x in min_args)) {
+ res = "undefined function '" x "'"
+ } else {
+ fun_top++
+ funs[fun_top] = x
+ accumulator[fun_top] = 636363
+ n_args[fun_top] = 0
+ }
+ } else if (match(input, "^[)] *")) {
+ input = substr(input, RLENGTH + 1)
+ res = process_end()
+ } else
+ res = "invalid input at '" substr(input, 1, 10) "...'"
+ }
+
+ if (res == "") {
+ if (fun_top != 0)
+ res = "unclosed '" funs[fun_top] "'"
+ else if (n_args[0] != 1)
+ res = "internal error, expected one result" \
+ ", got " n_args[0] " instead"
+ }
+
+ if (res == "")
+ pmrespond(accumulator[0])
+ else
+ pmrespond(res)
+}
+
+function process_argument (arg)
+{
+ if (fun_top == 0) {
+ if (n_args[0]++ != 0)
+ return "too many results, I only expect one"
+
+ accumulator[0] = arg
+ return ""
+ }
+
+ fun = funs[fun_top]
+ if (fun in max_args && max_args[fun] <= n_args[fun_top])
+ return "too many operands for " fun
+
+ if (fun == "int") {
+ accumulator[fun_top] = int(arg)
+ } else if (fun == "+") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] += arg
+ } else if (fun == "-") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] -= arg
+ } else if (fun == "*") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] *= arg
+ } else if (fun == "/") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (arg == 0)
+ return "division by zero"
+ else
+ accumulator[fun_top] /= arg
+ } else if (fun == "%") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (arg == 0)
+ return "division by zero"
+ else
+ accumulator[fun_top] %= arg
+ } else if (fun == "^" || fun == "**" || fun == "exp") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] ^= arg
+ } else if (fun == "sin") {
+ accumulator[fun_top] = sin(arg)
+ } else if (fun == "cos") {
+ accumulator[fun_top] = cos(arg)
+ } else if (fun == "atan2") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] = atan2(accumulator[fun_top], arg)
+ } else if (fun == "log") {
+ accumulator[fun_top] = log(arg)
+ } else if (fun == "rand") {
+ # Just for completeness, execution never gets here
+ } else if (fun == "sqrt") {
+ accumulator[fun_top] = sqrt(arg)
+ } else if (fun == "min") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (accumulator[fun_top] > arg)
+ accumulator[fun_top] = arg
+ } else if (fun == "max") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (accumulator[fun_top] < arg)
+ accumulator[fun_top] = arg
+ } else
+ return "internal error, unhandled operands for " fun
+
+ n_args[fun_top]++
+ return ""
+}
+
+function process_end ()
+{
+ if (fun_top <= 0)
+ return "extraneous ')'"
+
+ fun = funs[fun_top]
+ if (!(fun in min_args))
+ return "internal error, unhandled ')' for '" fun "'"
+ if (min_args[fun] > n_args[fun_top])
+ return "not enough operands for '" fun "'"
+
+ # There's no 'init' function to do it in
+ if (fun == "rand")
+ accumulator[fun_top] = rand()
+ else if (fun == "pi")
+ accumulator[fun_top] = 3.141592653589793
+ else if (fun == "e")
+ accumulator[fun_top] = 2.718281828459045
+
+ return process_argument(accumulator[fun_top--])
+}
+
+function get_config (key)
+{
+ print "XB get_config :" key
+ fflush("")
+
+ getline
+ parse($0)
+ return msg_param[0]
+}
+
+function parse (line, s, n, id, token)
+{
+ s = 1
+ id = 0
+
+ # NAWK only uses the first character of RS
+ if (line ~ /^\n/)
+ line = substr(line, 2)
+
+ msg_prefix = ""
+ msg_command = ""
+ delete msg_param
+
+ n = match(substr(line, s), / |$/)
+ while (n)
+ {
+ token = substr(line, s, n - 1)
+ if (token ~ /^:/)
+ {
+ if (s == 1)
+ msg_prefix = substr(token, 2)
+ else
+ {
+ msg_param[id] = substr(line, s + 1)
+ break
+ }
+ }
+ else if (!msg_command)
+ msg_command = toupper(token)
+ else
+ msg_param[id++] = token
+
+ s = s + n
+ n = index(substr(line, s), " ")
+
+ if (!n)
+ {
+ n = length(substr(line, s)) + 1
+ if (n == 1)
+ break;
+ }
+ }
+}
+
diff --git a/plugins/xB/factoids b/plugins/xB/factoids
new file mode 100755
index 0000000..84f6559
--- /dev/null
+++ b/plugins/xB/factoids
@@ -0,0 +1,177 @@
+#!/usr/bin/env perl
+#
+# xB factoids plugin
+#
+# Copyright 2016 Přemysl Eric Janouch <p@janouch.name>
+# See the file LICENSE for licensing information.
+#
+
+use strict;
+use warnings;
+use Text::Wrap;
+
+# --- IRC protocol -------------------------------------------------------------
+
+binmode STDIN; select STDIN; $| = 1; $/ = "\r\n";
+binmode STDOUT; select STDOUT; $| = 1; $\ = "\r\n";
+
+sub parse ($) {
+ chomp (my $line = shift);
+ return undef unless my ($nick, $user, $host, $command, $args) = ($line =~
+ qr/^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/o);
+ return {nick => $nick, user => $user, host => $host, command => $command,
+ args => defined $args ? [$args =~ /:?((?<=:).*|[^ ]+) */og] : []};
+}
+
+sub bot_print {
+ print "XB print :${\shift}";
+}
+
+# --- Initialization -----------------------------------------------------------
+
+my %config;
+for my $name (qw(prefix)) {
+ print "XB get_config :$name";
+ $config{$name} = (parse <STDIN>)->{args}->[0];
+}
+
+print "XB register";
+
+# --- Database -----------------------------------------------------------------
+# Simple map of (factoid_name => [definitions]); all factoids are separated
+# by newlines and definitions by carriage returns. Both disallowed in IRC.
+
+sub db_load {
+ local $/ = "\n";
+ my ($path) = @_;
+ open my $db, "<", $path or return {};
+
+ my %entries;
+ while (<$db>) {
+ chomp;
+ my @defs = split "\r";
+ $entries{shift @defs} = \@defs;
+ }
+ \%entries
+}
+
+sub db_save {
+ local $\ = "\n";
+ my ($path, $ref) = @_;
+ my $path_new = "$path.new";
+ open my $db, ">", $path_new or die "db save failed: $!";
+
+ my %entries = %$ref;
+ print $db join "\r", ($_, @{$entries{$_}}) for keys %entries;
+ close $db;
+ rename $path_new, $path or die "db save failed: $!";
+}
+
+# --- Factoids -----------------------------------------------------------------
+
+my $db_path = 'factoids.db';
+my %db = %{db_load $db_path};
+
+sub learn {
+ my ($respond, $input) = @_;
+ return &$respond("usage: <name> = <definition>")
+ unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
+
+ my ($name, $number, $definition) = ($1, $2, $3);
+ return &$respond("trailing numbers in names are disallowed")
+ if defined $2;
+ $db{$name} = [] unless exists $db{$name};
+
+ my $entries = $db{$name};
+ return &$respond("duplicate definition")
+ if grep { lc $_ eq lc $definition } @$entries;
+
+ push @$entries, $definition;
+ &$respond("saved as #${\scalar @$entries}");
+ db_save $db_path, \%db;
+}
+
+sub check_number {
+ my ($respond, $name, $number) = @_;
+ my $entries = $db{$name};
+ if ($number > @$entries) {
+ &$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
+ } elsif (not $number) {
+ &$respond("number must not be zero");
+ } else {
+ return 1;
+ }
+ return 0;
+}
+
+sub forget {
+ my ($respond, $input) = @_;
+ return &$respond("usage: <name> <number>")
+ unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
+
+ my ($name, $number) = ($1, int($2));
+ return &$respond(qq/"$name" is undefined/)
+ unless exists $db{$name};
+
+ my $entries = $db{$name};
+ return unless check_number $respond, $name, $number;
+
+ splice @$entries, --$number, 1;
+ &$respond("forgotten");
+ db_save $db_path, \%db;
+}
+
+sub whatis {
+ my ($respond, $input) = @_;
+ return &$respond("usage: <name> [<number>]")
+ unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
+
+ my ($name, $number) = ($1, $2);
+ return &$respond(qq/"$name" is undefined/)
+ unless exists $db{$name};
+
+ my $entries = $db{$name};
+ if (defined $number) {
+ return unless check_number $respond, $name, $number;
+ &$respond(qq/"$name" is #$number $entries->[$number - 1]/);
+ } else {
+ my $i = 1;
+ my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
+ &$respond(qq/"$name" is $definition/);
+ }
+}
+
+sub wildcard {
+ my ($respond, $input) = @_;
+ $input =~ /=/ ? learn(@_) : whatis(@_);
+}
+
+my %commands = (
+ 'learn' => \&learn,
+ 'forget' => \&forget,
+ 'whatis' => \&whatis,
+ '??' => \&wildcard,
+);
+
+# --- Input loop ---------------------------------------------------------------
+
+while (my $line = <STDIN>) {
+ my %msg = %{parse $line};
+ my @args = @{$msg{args}};
+
+ # This plugin only bothers to respond to PRIVMSG messages
+ next unless $msg{command} eq 'PRIVMSG' and @args >= 2
+ and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
+
+ # So far the only reaction is a PRIVMSG back to the sender, so all the
+ # handlers need is a response callback and all arguments to the command
+ my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
+ ? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
+ # Wrap all responses so that there's space for our prefix in the message
+ my $respond = sub {
+ local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
+ my $start = "PRIVMSG $target :$quote";
+ print for split "\n", wrap $start, $start, shift;
+ };
+ &{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
+}
diff --git a/plugins/xB/pomodoro b/plugins/xB/pomodoro
new file mode 100755
index 0000000..910f707
--- /dev/null
+++ b/plugins/xB/pomodoro
@@ -0,0 +1,502 @@
+#!/usr/bin/env ruby
+# coding: utf-8
+#
+# xB pomodoro plugin
+#
+# Copyright 2015 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+# --- Simple event loop --------------------------------------------------------
+
+# This is more or less a straight-forward port of my C event loop. It's a bit
+# unfortunate that I really have to implement all this in order to get some
+# basic asynchronicity but at least I get to exercise my Ruby.
+
+class TimerEvent
+ attr_accessor :index, :when, :callback
+
+ def initialize (callback)
+ raise ArgumentError unless callback.is_a? Proc
+
+ @index = nil
+ @when = nil
+ @callback = callback
+ end
+
+ def active?
+ @index != nil
+ end
+
+ def until
+ return @when - Time.new
+ end
+end
+
+class IOEvent
+ READ = 1 << 0
+ WRITE = 1 << 1
+
+ attr_accessor :read_index, :write_index, :io, :callback
+
+ def initialize (io, callback)
+ raise ArgumentError unless callback.is_a? Proc
+
+ @read_index = nil
+ @write_index = nil
+ @io = io
+ @callback = callback
+ end
+end
+
+class EventLoop
+ def initialize
+ @running = false
+ @timers = []
+ @readers = []
+ @writers = []
+ @io_to_event = {}
+ end
+
+ def set_timer (timer, timeout)
+ raise ArgumentError unless timer.is_a? TimerEvent
+
+ timer.when = Time.now + timeout
+ if timer.index
+ heapify_down timer.index
+ heapify_up timer.index
+ else
+ timer.index = @timers.size
+ @timers.push timer
+ heapify_up timer.index
+ end
+ end
+
+ def reset_timer (timer)
+ raise ArgumentError unless timer.is_a? TimerEvent
+ remove_timer_at timer.index if timer.index
+ end
+
+ def set_io (io_event, events)
+ raise ArgumentError unless io_event.is_a? IOEvent
+ raise ArgumentError unless events.is_a? Numeric
+
+ reset_io io_event
+
+ @io_to_event[io_event.io] = io_event
+ if events & IOEvent::READ
+ io_event.read_index = @readers.size
+ @readers.push io_event.io
+ end
+ if events & IOEvent::WRITE
+ io_event.read_index = @writers.size
+ @writers.push io_event.io
+ end
+ end
+
+ def reset_io (io_event)
+ raise ArgumentError unless io_event.is_a? IOEvent
+
+ @readers.delete_at io_event.read_index if io_event.read_index
+ @writers.delete_at io_event.write_index if io_event.write_index
+
+ io_event.read_index = nil
+ io_event.write_index = nil
+
+ @io_to_event.delete io_event.io
+ end
+
+ def run
+ @running = true
+ while @running do one_iteration end
+ end
+
+ def quit
+ @running = false
+ end
+
+private
+ def one_iteration
+ rs, ws, = IO.select @readers, @writers, [], nearest_timeout
+ dispatch_timers
+ (Array(rs) | Array(ws)).each do |io|
+ @io_to_event[io].callback.call io
+ end
+ end
+
+ def dispatch_timers
+ now = Time.new
+ while not @timers.empty? and @timers[0].when <= now do
+ @timers[0].callback.call
+ remove_timer_at 0
+ end
+ end
+
+ def nearest_timeout
+ return nil if @timers.empty?
+ timeout = @timers[0].until
+ if timeout < 0 then 0 else timeout end
+ end
+
+ def remove_timer_at (index)
+ @timers[index].index = nil
+ moved = @timers.pop
+ return if index == @timers.size
+
+ @timers[index] = moved
+ @timers[index].index = index
+ heapify_down index
+ end
+
+ def swap_timers (a, b)
+ @timers[a], @timers[b] = @timers[b], @timers[a]
+ @timers[a].index = a
+ @timers[b].index = b
+ end
+
+ def heapify_up (index)
+ while index != 0 do
+ parent = (index - 1) / 2
+ break if @timers[parent].when <= @timers[index].when
+ swap_timers index, parent
+ index = parent
+ end
+ end
+
+ def heapify_down (index)
+ loop do
+ parent = index
+ left = 2 * index + 1
+ right = 2 * index + 2
+
+ lowest = parent
+ lowest = left if left < @timers.size and
+ @timers[left] .when < @timers[lowest].when
+ lowest = right if right < @timers.size and
+ @timers[right].when < @timers[lowest].when
+ break if parent == lowest
+
+ swap_timers lowest, parent
+ index = lowest
+ end
+ end
+end
+
+# --- IRC protocol -------------------------------------------------------------
+
+$stdin.set_encoding 'ASCII-8BIT'
+$stdout.set_encoding 'ASCII-8BIT'
+
+$stdin.sync = true
+$stdout.sync = true
+
+$/ = "\r\n"
+$\ = "\r\n"
+
+RE_MSG = /(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/
+RE_ARGS = /:?((?<=:).*|[^ ]+) */
+
+def parse (line)
+ m = line.match RE_MSG
+ return nil if not m
+
+ nick, user, host, command, args = *m.captures
+ args = if args then args.scan(RE_ARGS).flatten else [] end
+ [nick, user, host, command, args]
+end
+
+def bot_print (what)
+ print "XB print :#{what}"
+end
+
+# --- Initialization -----------------------------------------------------------
+
+# We can only read in configuration from here so far
+# To read it from anywhere else, it has to be done asynchronously
+$config = {}
+[:prefix].each do |name|
+ print "XB get_config :#{name}"
+ _, _, _, _, args = *parse($stdin.gets.chomp)
+ $config[name] = args[0]
+end
+
+print "XB register"
+
+# --- Plugin logic -------------------------------------------------------------
+
+# FIXME: this needs a major refactor as it doesn't make much sense at all
+
+class MessageMeta < Struct.new(:nick, :user, :host, :channel, :ctx, :quote)
+ def respond (message)
+ print "PRIVMSG #{ctx} :#{quote}#{message}"
+ end
+end
+
+class Context
+ attr_accessor :nick, :ctx
+
+ def initialize (meta)
+ @nick = meta.nick
+ @ctx = meta.ctx
+ end
+
+ def == (other)
+ self.class == other.class \
+ and other.nick == @nick \
+ and other.ctx == @ctx
+ end
+
+ alias eql? ==
+
+ def hash
+ @nick.hash ^ @ctx.hash
+ end
+end
+
+class PomodoroTimer
+ def initialize (context)
+ @ctx = context.ctx
+ @nicks = [context.nick]
+
+ @timer_work = TimerEvent.new(lambda { on_work })
+ @timer_rest = TimerEvent.new(lambda { on_rest })
+
+ on_work
+ end
+
+ def inform (message)
+ # FIXME: it tells the nick even in PM's
+ quote = "#{@nicks.join(" ")}: "
+ print "PRIVMSG #{@ctx} :#{quote}#{message}"
+ end
+
+ def on_work
+ inform "work now!"
+ $loop.set_timer @timer_rest, 25 * 60
+ end
+
+ def on_rest
+ inform "rest now!"
+ $loop.set_timer @timer_work, 5 * 60
+ end
+
+ def join (meta)
+ return if @nicks.include? meta.nick
+
+ meta.respond "you have joined their pomodoro"
+ @nicks |= [meta.nick]
+ end
+
+ def part (meta, requested)
+ return if not @nicks.include? meta.nick
+
+ if requested
+ meta.respond "you have stopped your pomodoro"
+ end
+
+ @nicks -= [meta.nick]
+ if @nicks.empty?
+ $loop.reset_timer @timer_work
+ $loop.reset_timer @timer_rest
+ end
+ end
+
+ def status (meta)
+ return if not @nicks.include? meta.nick
+
+ if @timer_rest.active?
+ till = @timer_rest.until
+ meta.respond "working, #{(till / 60).to_i} minutes, " +
+ "#{(till % 60).to_i} seconds until rest"
+ end
+ if @timer_work.active?
+ till = @timer_work.until
+ meta.respond "resting, #{(till / 60).to_i} minutes, " +
+ "#{(till % 60).to_i} seconds until work"
+ end
+ end
+end
+
+class Pomodoro
+ KEYWORD = "pomodoro"
+
+ def initialize
+ @timers = {}
+ end
+
+ def on_help (meta, args)
+ meta.respond "usage: #{KEYWORD} { start | stop | join <nick> | status }"
+ end
+
+ def on_start (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} start"
+ return
+ end
+
+ context = Context.new meta
+ if @timers[context]
+ meta.respond "you already have a timer running here"
+ else
+ @timers[context] = PomodoroTimer.new meta
+ end
+ end
+
+ def on_join (meta, args)
+ if args.size != 1
+ meta.respond "usage: #{KEYWORD} join <nick>"
+ return
+ end
+
+ context = Context.new meta
+ if @timers[context]
+ meta.respond "you already have a timer running here"
+ return
+ end
+
+ joined_context = Context.new meta
+ joined_context.nick = args.shift
+ timer = @timers[joined_context]
+ if not timer
+ meta.respond "that person doesn't have a timer here"
+ else
+ timer.join meta
+ @timers[context] = timer
+ end
+ end
+
+ def on_stop (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} stop"
+ return
+ end
+
+ context = Context.new meta
+ timer = @timers[context]
+ if not timer
+ meta.respond "you don't have a timer running here"
+ else
+ timer.part meta, true
+ @timers.delete context
+ end
+ end
+
+ def on_status (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} status"
+ return
+ end
+
+ timer = @timers[Context.new meta]
+ if not timer
+ meta.respond "you don't have a timer running here"
+ else
+ timer.status meta
+ end
+ end
+
+ def process_command (meta, msg)
+ args = msg.split
+ return if args.shift != KEYWORD
+
+ method = "on_#{args.shift}"
+ send method, meta, args if respond_to? method
+ end
+
+ def on_server_nick (meta, command, args)
+ # TODO: either handle this properly...
+ happened = false
+ @timers.keys.each do |key|
+ next if key.nick != meta.nick
+ @timers[key].part meta, false
+ @timers.delete key
+ happened = true
+ end
+ if happened
+ # TODO: ...or at least inform the user via his new nick
+ end
+ end
+
+ def on_server_part (meta, command, args)
+ # TODO: instead of cancelling the user's pomodoros, either redirect
+ # them to PM's and later upon rejoining undo the redirection...
+ context = Context.new(meta)
+ context.ctx = meta.channel
+ if @timers.include? context
+ # TODO: ...or at least inform the user about the cancellation
+ @timers[context].part meta, false
+ @timers.delete context
+ end
+ end
+
+ def on_server_quit (meta, command, args)
+ @timers.keys.each do |key|
+ next if key.nick != meta.nick
+ @timers[key].part meta, false
+ @timers.delete key
+ end
+ end
+
+ def process (meta, command, args)
+ method = "on_server_#{command.downcase}"
+ send method, meta, command, args if respond_to? method
+ end
+end
+
+# --- IRC message processing ---------------------------------------------------
+
+$handlers = [Pomodoro.new]
+def process_line (line)
+ msg = parse line
+ return if not msg
+
+ nick, user, host, command, args = *msg
+
+ context = nick
+ quote = ""
+ channel = nil
+
+ if args.size >= 1 and args[0].start_with? ?#, ?+, ?&, ?!
+ case command
+ when "PRIVMSG", "NOTICE", "JOIN"
+ context = args[0]
+ quote = "#{nick}: "
+ channel = args[0]
+ when "PART"
+ channel = args[0]
+ end
+ end
+
+ # Handle any IRC message
+ meta = MessageMeta.new(nick, user, host, channel, context, quote).freeze
+ $handlers.each do |handler|
+ handler.process meta, command, args
+ end
+
+ # Handle pre-processed bot commands
+ if command == 'PRIVMSG' and args.size >= 2
+ msg = args[1]
+ return unless msg.start_with? $config[:prefix]
+ $handlers.each do |handler|
+ handler.process_command meta, msg[$config[:prefix].size..-1]
+ end
+ end
+end
+
+buffer = ""
+stdin_io = IOEvent.new($stdin, lambda do |io|
+ begin
+ buffer << io.read_nonblock(4096)
+ lines = buffer.split $/, -1
+ buffer = lines.pop
+ lines.each { |line| process_line line }
+ rescue EOFError
+ $loop.quit
+ rescue IO::WaitReadable
+ # Ignore
+ end
+end)
+
+$loop = EventLoop.new
+$loop.set_io stdin_io, IOEvent::READ
+$loop.run
diff --git a/plugins/xB/script b/plugins/xB/script
new file mode 100755
index 0000000..43bd66f
--- /dev/null
+++ b/plugins/xB/script
@@ -0,0 +1,2310 @@
+#!/usr/bin/tcc -run -lm
+//
+// xB scripting plugin, using a custom stack-based language
+//
+// Copyright 2014 Přemysl Eric Janouch
+// See the file LICENSE for licensing information.
+//
+// Just compile this file as usual (sans #!) if you don't feel like using TCC.
+// It is a very basic and portable C99 application. It's not supposed to be
+// very sophisticated, for it'd get extremely big.
+//
+// The main influences of the language were Factor and Joy, stripped of all
+// even barely complex stuff. In its current state, it's only really useful as
+// a calculator but it's got great potential for extending.
+//
+// If you don't like something, just change it; this is just an experiment.
+//
+// NOTE: it is relatively easy to abuse. Be careful.
+//
+
+#define _XOPEN_SOURCE 500
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <time.h>
+#include <stdbool.h>
+#include <strings.h>
+#include <math.h>
+
+#define ADDRESS_SPACE_LIMIT (100 * 1024 * 1024)
+#include <sys/resource.h>
+
+#if defined __GNUC__
+#define ATTRIBUTE_PRINTF(x, y) __attribute__ ((format (printf, x, y)))
+#else // ! __GNUC__
+#define ATTRIBUTE_PRINTF(x, y)
+#endif // ! __GNUC__
+
+#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
+
+// --- Utilities ---------------------------------------------------------------
+
+static char *strdup_printf (const char *format, ...) ATTRIBUTE_PRINTF (1, 2);
+
+static char *
+strdup_vprintf (const char *format, va_list ap)
+{
+ va_list aq;
+ va_copy (aq, ap);
+ int size = vsnprintf (NULL, 0, format, aq);
+ va_end (aq);
+ if (size < 0)
+ return NULL;
+
+ char buf[size + 1];
+ size = vsnprintf (buf, sizeof buf, format, ap);
+ if (size < 0)
+ return NULL;
+
+ return strdup (buf);
+}
+
+static char *
+strdup_printf (const char *format, ...)
+{
+ va_list ap;
+ va_start (ap, format);
+ char *result = strdup_vprintf (format, ap);
+ va_end (ap);
+ return result;
+}
+
+// --- Generic buffer ----------------------------------------------------------
+
+struct buffer
+{
+ char *s; ///< Buffer data
+ size_t alloc; ///< Number of bytes allocated
+ size_t len; ///< Number of bytes used
+ bool memory_failure; ///< Memory allocation failed
+};
+
+#define BUFFER_INITIALIZER { NULL, 0, 0, false }
+
+static bool
+buffer_append (struct buffer *self, const void *s, size_t n)
+{
+ if (self->memory_failure)
+ return false;
+
+ if (!self->s)
+ self->s = malloc (self->alloc = 8);
+ while (self->len + n > self->alloc)
+ self->s = realloc (self->s, self->alloc <<= 1);
+
+ if (!self->s)
+ {
+ self->memory_failure = true;
+ return false;
+ }
+
+ memcpy (self->s + self->len, s, n);
+ self->len += n;
+ return true;
+}
+
+inline static bool
+buffer_append_c (struct buffer *self, char c)
+{
+ return buffer_append (self, &c, 1);
+}
+
+// --- Data types --------------------------------------------------------------
+
+enum item_type
+{
+ ITEM_STRING,
+ ITEM_WORD,
+ ITEM_INTEGER,
+ ITEM_FLOAT,
+ ITEM_LIST
+};
+
+struct item
+{
+#define ITEM_HEADER \
+ enum item_type type; /**< The type of this object */ \
+ struct item *next; /**< Next item on the list/stack */
+
+ ITEM_HEADER
+};
+
+struct item_string
+{
+ ITEM_HEADER
+ size_t len; ///< Length of the string (sans '\0')
+ char value[]; ///< The null-terminated string value
+};
+
+#define get_string(item) \
+ (assert ((item)->type == ITEM_STRING), \
+ ((struct item_string *)(item))->value)
+
+/// It looks like a string but it doesn't quack like a string
+#define item_word item_string
+
+#define get_word(item) \
+ (assert ((item)->type == ITEM_WORD), \
+ ((struct item_word *)(item))->value)
+
+struct item_integer
+{
+ ITEM_HEADER
+ long long value; ///< The integer value
+};
+
+#define get_integer(item) \
+ (assert ((item)->type == ITEM_INTEGER), \
+ ((struct item_integer *)(item))->value)
+
+struct item_float
+{
+ ITEM_HEADER
+ long double value; ///< The floating point value
+};
+
+#define get_float(item) \
+ (assert ((item)->type == ITEM_FLOAT), \
+ ((struct item_float *)(item))->value)
+
+struct item_list
+{
+ ITEM_HEADER
+ struct item *head; ///< The head of the list
+};
+
+#define get_list(item) \
+ (assert ((item)->type == ITEM_LIST), \
+ ((struct item_list *)(item))->head)
+
+#define set_list(item, head_) \
+ (assert ((item)->type == ITEM_LIST), \
+ item_free_list (((struct item_list *)(item))->head), \
+ ((struct item_list *)(item))->head = (head_))
+
+const char *
+item_type_to_str (enum item_type type)
+{
+ switch (type)
+ {
+ case ITEM_STRING: return "string";
+ case ITEM_WORD: return "word";
+ case ITEM_INTEGER: return "integer";
+ case ITEM_FLOAT: return "float";
+ case ITEM_LIST: return "list";
+ }
+ abort ();
+}
+
+// --- Item management ---------------------------------------------------------
+
+static void item_free_list (struct item *);
+static struct item *new_clone_list (const struct item *);
+
+static void
+item_free (struct item *item)
+{
+ if (item->type == ITEM_LIST)
+ item_free_list (get_list (item));
+ free (item);
+}
+
+static void
+item_free_list (struct item *item)
+{
+ while (item)
+ {
+ struct item *link = item;
+ item = item->next;
+ item_free (link);
+ }
+}
+
+static struct item *
+new_clone (const struct item *item)
+{
+ size_t size;
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ case ITEM_WORD:
+ {
+ const struct item_string *x = (const struct item_string *) item;
+ size = sizeof *x + x->len + 1;
+ break;
+ }
+ case ITEM_INTEGER: size = sizeof (struct item_integer); break;
+ case ITEM_FLOAT: size = sizeof (struct item_float); break;
+ case ITEM_LIST: size = sizeof (struct item_list); break;
+ }
+
+ struct item *clone = malloc (size);
+ if (!clone)
+ return NULL;
+
+ memcpy (clone, item, size);
+ if (item->type == ITEM_LIST)
+ {
+ struct item_list *x = (struct item_list *) clone;
+ if (x->head && !(x->head = new_clone_list (x->head)))
+ {
+ free (clone);
+ return NULL;
+ }
+ }
+ clone->next = NULL;
+ return clone;
+}
+
+static struct item *
+new_clone_list (const struct item *item)
+{
+ struct item *head = NULL, *clone;
+ for (struct item **out = &head; item; item = item->next)
+ {
+ if (!(clone = *out = new_clone (item)))
+ {
+ item_free_list (head);
+ return NULL;
+ }
+ clone->next = NULL;
+ out = &clone->next;
+ }
+ return head;
+}
+
+static struct item *
+new_string (const char *s, ssize_t len)
+{
+ if (len < 0)
+ len = strlen (s);
+
+ struct item_string *item = calloc (1, sizeof *item + len + 1);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_STRING;
+ item->len = len;
+ memcpy (item->value, s, len);
+ item->value[len] = '\0';
+ return (struct item *) item;
+}
+
+static struct item *
+new_word (const char *s, ssize_t len)
+{
+ struct item *item = new_string (s, len);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_WORD;
+ return item;
+}
+
+static struct item *
+new_integer (long long value)
+{
+ struct item_integer *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_INTEGER;
+ item->value = value;
+ return (struct item *) item;
+}
+
+static struct item *
+new_float (long double value)
+{
+ struct item_float *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_FLOAT;
+ item->value = value;
+ return (struct item *) item;
+}
+
+static struct item *
+new_list (struct item *head)
+{
+ struct item_list *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_LIST;
+ item->head = head;
+ return (struct item *) item;
+}
+
+// --- Parsing -----------------------------------------------------------------
+
+#define PARSE_ERROR_TABLE(XX) \
+ XX( OK, NULL ) \
+ XX( EOF, "unexpected end of input" ) \
+ XX( INVALID_HEXA_ESCAPE, "invalid hexadecimal escape sequence" ) \
+ XX( INVALID_ESCAPE, "unrecognized escape sequence" ) \
+ XX( MEMORY, "memory allocation failure" ) \
+ XX( FLOAT_RANGE, "floating point value out of range" ) \
+ XX( INTEGER_RANGE, "integer out of range" ) \
+ XX( INVALID_INPUT, "invalid input" ) \
+ XX( UNEXPECTED_INPUT, "unexpected input" )
+
+enum tokenizer_error
+{
+#define XX(x, y) PARSE_ERROR_ ## x,
+ PARSE_ERROR_TABLE (XX)
+#undef XX
+ PARSE_ERROR_COUNT
+};
+
+struct tokenizer
+{
+ const char *cursor;
+ enum tokenizer_error error;
+};
+
+static bool
+decode_hexa_escape (struct tokenizer *self, struct buffer *buf)
+{
+ int i;
+ char c, code = 0;
+
+ for (i = 0; i < 2; i++)
+ {
+ c = tolower (*self->cursor);
+ if (c >= '0' && c <= '9')
+ code = (code << 4) | (c - '0');
+ else if (c >= 'a' && c <= 'f')
+ code = (code << 4) | (c - 'a' + 10);
+ else
+ break;
+
+ self->cursor++;
+ }
+
+ if (!i)
+ return false;
+
+ buffer_append_c (buf, code);
+ return true;
+}
+
+static bool
+decode_octal_escape (struct tokenizer *self, struct buffer *buf)
+{
+ int i;
+ char c, code = 0;
+
+ for (i = 0; i < 3; i++)
+ {
+ c = *self->cursor;
+ if (c < '0' || c > '7')
+ break;
+
+ code = (code << 3) | (c - '0');
+ self->cursor++;
+ }
+
+ if (!i)
+ return false;
+
+ buffer_append_c (buf, code);
+ return true;
+}
+
+static bool
+decode_escape_sequence (struct tokenizer *self, struct buffer *buf)
+{
+ // Support some basic escape sequences from the C language
+ char c;
+ switch ((c = *self->cursor))
+ {
+ case '\0':
+ self->error = PARSE_ERROR_EOF;
+ return false;
+ case 'x':
+ case 'X':
+ self->cursor++;
+ if (decode_hexa_escape (self, buf))
+ return true;
+
+ self->error = PARSE_ERROR_INVALID_HEXA_ESCAPE;
+ return false;
+ default:
+ if (decode_octal_escape (self, buf))
+ return true;
+
+ self->cursor++;
+ const char *from = "abfnrtv\"\\", *to = "\a\b\f\n\r\t\v\"\\", *x;
+ if ((x = strchr (from, c)))
+ {
+ buffer_append_c (buf, to[x - from]);
+ return true;
+ }
+
+ self->error = PARSE_ERROR_INVALID_ESCAPE;
+ return false;
+ }
+}
+
+static struct item *
+parse_string (struct tokenizer *self)
+{
+ struct buffer buf = BUFFER_INITIALIZER;
+ struct item *item = NULL;
+ char c;
+
+ while (true)
+ switch ((c = *self->cursor++))
+ {
+ case '\0':
+ self->cursor--;
+ self->error = PARSE_ERROR_EOF;
+ goto end;
+ case '"':
+ if (buf.memory_failure
+ || !(item = new_string (buf.s, buf.len)))
+ self->error = PARSE_ERROR_MEMORY;
+ goto end;
+ case '\\':
+ if (decode_escape_sequence (self, &buf))
+ break;
+ goto end;
+ default:
+ buffer_append_c (&buf, c);
+ }
+
+end:
+ free (buf.s);
+ return item;
+}
+
+static struct item *
+try_parse_number (struct tokenizer *self)
+{
+ // These two standard library functions can digest a lot of various inputs,
+ // including NaN and +/- infinity. That may get a bit confusing.
+ char *float_end;
+ errno = 0;
+ long double float_value = strtold (self->cursor, &float_end);
+ int float_errno = errno;
+
+ char *int_end;
+ errno = 0;
+ long long int_value = strtoll (self->cursor, &int_end, 10);
+ int int_errno = errno;
+
+ // If they both fail, then this is most probably not a number.
+ if (float_end == int_end && float_end == self->cursor)
+ return NULL;
+
+ // Only use the floating point result if it parses more characters:
+ struct item *item;
+ if (float_end > int_end)
+ {
+ if (float_errno == ERANGE)
+ {
+ self->error = PARSE_ERROR_FLOAT_RANGE;
+ return NULL;
+ }
+ self->cursor = float_end;
+ if (!(item = new_float (float_value)))
+ self->error = PARSE_ERROR_MEMORY;
+ return item;
+ }
+ else
+ {
+ if (int_errno == ERANGE)
+ {
+ self->error = PARSE_ERROR_INTEGER_RANGE;
+ return NULL;
+ }
+ self->cursor = int_end;
+ if (!(item = new_integer (int_value)))
+ self->error = PARSE_ERROR_MEMORY;
+ return item;
+ }
+}
+
+static struct item *
+parse_word (struct tokenizer *self)
+{
+ struct buffer buf = BUFFER_INITIALIZER;
+ struct item *item = NULL;
+ char c;
+
+ // Here we accept almost anything that doesn't break the grammar
+ while (!strchr (" []\"", (c = *self->cursor++)) && (unsigned char) c > ' ')
+ buffer_append_c (&buf, c);
+ self->cursor--;
+
+ if (buf.memory_failure)
+ self->error = PARSE_ERROR_MEMORY;
+ else if (!buf.len)
+ self->error = PARSE_ERROR_INVALID_INPUT;
+ else if (!(item = new_word (buf.s, buf.len)))
+ self->error = PARSE_ERROR_MEMORY;
+
+ free (buf.s);
+ return item;
+}
+
+static struct item *parse_item_list (struct tokenizer *);
+
+static struct item *
+parse_list (struct tokenizer *self)
+{
+ struct item *list = parse_item_list (self);
+ if (self->error)
+ {
+ assert (list == NULL);
+ return NULL;
+ }
+ if (!*self->cursor)
+ {
+ self->error = PARSE_ERROR_EOF;
+ item_free_list (list);
+ return NULL;
+ }
+ assert (*self->cursor == ']');
+ self->cursor++;
+ return new_list (list);
+}
+
+static struct item *
+parse_item (struct tokenizer *self)
+{
+ char c;
+ switch ((c = *self->cursor++))
+ {
+ case '[': return parse_list (self);
+ case '"': return parse_string (self);
+ default:;
+ }
+
+ self->cursor--;
+ struct item *item = try_parse_number (self);
+ if (!item && !self->error)
+ item = parse_word (self);
+ return item;
+}
+
+static struct item *
+parse_item_list (struct tokenizer *self)
+{
+ struct item *head = NULL;
+ struct item **tail = &head;
+
+ char c;
+ bool expected = true;
+ while ((c = *self->cursor) && c != ']')
+ {
+ if (isspace (c))
+ {
+ self->cursor++;
+ expected = true;
+ continue;
+ }
+ else if (!expected)
+ {
+ self->error = PARSE_ERROR_UNEXPECTED_INPUT;
+ goto fail;
+ }
+
+ if (!(*tail = parse_item (self)))
+ goto fail;
+ tail = &(*tail)->next;
+ expected = false;
+ }
+ return head;
+
+fail:
+ item_free_list (head);
+ return NULL;
+}
+
+static struct item *
+parse (const char *s, const char **error)
+{
+ struct tokenizer self = { .cursor = s, .error = PARSE_ERROR_OK };
+ struct item *list = parse_item_list (&self);
+ if (!self.error && *self.cursor != '\0')
+ {
+ self.error = PARSE_ERROR_UNEXPECTED_INPUT;
+ item_free_list (list);
+ list = NULL;
+ }
+
+#define XX(x, y) y,
+ static const char *strings[PARSE_ERROR_COUNT] =
+ { PARSE_ERROR_TABLE (XX) };
+#undef XX
+
+ static char error_buf[128];
+ if (self.error && error)
+ {
+ snprintf (error_buf, sizeof error_buf, "at character %d: %s",
+ (int) (self.cursor - s) + 1, strings[self.error]);
+ *error = error_buf;
+ }
+ return list;
+}
+
+// --- Runtime -----------------------------------------------------------------
+
+// TODO: try to think of a _simple_ way to do preemptive multitasking
+
+struct context
+{
+ struct item *stack; ///< The current top of the stack
+ size_t stack_size; ///< Number of items on the stack
+
+ size_t reduction_count; ///< # of function calls so far
+ size_t reduction_limit; ///< The hard limit on function calls
+
+ char *error; ///< Error information
+ bool error_is_fatal; ///< Whether the error can be catched
+ bool memory_failure; ///< Memory allocation failure
+
+ void *user_data; ///< User data
+};
+
+/// Internal handler for a function
+typedef bool (*handler_fn) (struct context *);
+
+struct fn
+{
+ struct fn *next; ///< The next link in the chain
+
+ handler_fn handler; ///< Internal C handler, or NULL
+ struct item *script; ///< Alternatively runtime code
+ char name[]; ///< The name of the function
+};
+
+struct fn *g_functions; ///< Maps words to functions
+
+static void
+context_init (struct context *ctx)
+{
+ ctx->stack = NULL;
+ ctx->stack_size = 0;
+
+ ctx->reduction_count = 0;
+ ctx->reduction_limit = 2000;
+
+ ctx->error = NULL;
+ ctx->error_is_fatal = false;
+ ctx->memory_failure = false;
+
+ ctx->user_data = NULL;
+}
+
+static void
+context_free (struct context *ctx)
+{
+ item_free_list (ctx->stack);
+ ctx->stack = NULL;
+
+ free (ctx->error);
+ ctx->error = NULL;
+}
+
+static bool
+set_error (struct context *ctx, const char *format, ...)
+{
+ free (ctx->error);
+
+ va_list ap;
+ va_start (ap, format);
+ ctx->error = strdup_vprintf (format, ap);
+ va_end (ap);
+
+ if (!ctx->error)
+ ctx->memory_failure = true;
+ return false;
+}
+
+static bool
+push (struct context *ctx, struct item *item)
+{
+ // The `item' is typically a result from new_<type>(), thus when it is null,
+ // that function must have failed. This is a shortcut for convenience.
+ if (!item)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ assert (item->next == NULL);
+ item->next = ctx->stack;
+ ctx->stack = item;
+ ctx->stack_size++;
+ return true;
+}
+
+static bool
+bump_reductions (struct context *ctx)
+{
+ if (++ctx->reduction_count >= ctx->reduction_limit)
+ {
+ ctx->error_is_fatal = true;
+ return set_error (ctx, "reduction limit reached");
+ }
+ return true;
+}
+
+static bool execute (struct context *, struct item *);
+
+static bool
+call_function (struct context *ctx, const char *name)
+{
+ struct fn *iter;
+ for (iter = g_functions; iter; iter = iter->next)
+ if (!strcmp (name, iter->name))
+ goto found;
+ return set_error (ctx, "unknown function: %s", name);
+
+found:
+ if (!bump_reductions (ctx))
+ return false;
+
+ if (iter->handler
+ ? iter->handler (ctx)
+ : execute (ctx, iter->script))
+ return true;
+
+ // In this case, `error' is NULL
+ if (ctx->memory_failure)
+ return false;
+
+ // This creates some form of a stack trace
+ char *tmp = ctx->error;
+ ctx->error = NULL;
+ set_error (ctx, "%s -> %s", name, tmp);
+ free (tmp);
+ return false;
+}
+
+static void
+free_function (struct fn *fn)
+{
+ item_free_list (fn->script);
+ free (fn);
+}
+
+static void
+unregister_function (const char *name)
+{
+ for (struct fn **iter = &g_functions; *iter; iter = &(*iter)->next)
+ if (!strcmp ((*iter)->name, name))
+ {
+ struct fn *tmp = *iter;
+ *iter = tmp->next;
+ free_function (tmp);
+ break;
+ }
+}
+
+static struct fn *
+prepend_new_fn (const char *name)
+{
+ struct fn *fn = calloc (1, sizeof *fn + strlen (name) + 1);
+ if (!fn)
+ return NULL;
+
+ strcpy (fn->name, name);
+ fn->next = g_functions;
+ return g_functions = fn;
+}
+
+static bool
+register_handler (const char *name, handler_fn handler)
+{
+ unregister_function (name);
+ struct fn *fn = prepend_new_fn (name);
+ if (!fn)
+ return false;
+ fn->handler = handler;
+ return true;
+}
+
+static bool
+register_script (const char *name, struct item *script)
+{
+ unregister_function (name);
+ struct fn *fn = prepend_new_fn (name);
+ if (!fn)
+ return false;
+ fn->script = script;
+ return true;
+}
+
+static bool
+execute (struct context *ctx, struct item *script)
+{
+ for (; script; script = script->next)
+ {
+ if (script->type != ITEM_WORD)
+ {
+ if (!bump_reductions (ctx)
+ || !push (ctx, new_clone (script)))
+ return false;
+ }
+ else if (!call_function (ctx, get_word (script)))
+ return false;
+ }
+ return true;
+}
+
+// --- Runtime library ---------------------------------------------------------
+
+#define defn(name) static bool name (struct context *ctx)
+
+#define check_stack(n) \
+ if (ctx->stack_size < n) { \
+ set_error (ctx, "stack underflow"); \
+ return 0; \
+ }
+
+inline static bool
+check_stack_safe (struct context *ctx, size_t n)
+{
+ check_stack (n);
+ return true;
+}
+
+static bool
+check_type (struct context *ctx, const void *item_, enum item_type type)
+{
+ const struct item *item = item_;
+ if (item->type == type)
+ return true;
+
+ return set_error (ctx, "invalid type: expected `%s', got `%s'",
+ item_type_to_str (type), item_type_to_str (item->type));
+}
+
+static struct item *
+pop (struct context *ctx)
+{
+ check_stack (1);
+ struct item *top = ctx->stack;
+ ctx->stack = top->next;
+ top->next = NULL;
+ ctx->stack_size--;
+ return top;
+}
+
+// - - Types - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define defn_is_type(name, item_type) \
+ defn (fn_is_##name) { \
+ check_stack (1); \
+ struct item *top = pop (ctx); \
+ push (ctx, new_integer (top->type == (item_type))); \
+ item_free (top); \
+ return true; \
+ }
+
+defn_is_type (string, ITEM_STRING)
+defn_is_type (word, ITEM_WORD)
+defn_is_type (integer, ITEM_INTEGER)
+defn_is_type (float, ITEM_FLOAT)
+defn_is_type (list, ITEM_LIST)
+
+defn (fn_to_string)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ char *value;
+
+ switch (item->type)
+ {
+ case ITEM_WORD:
+ item->type = ITEM_STRING;
+ case ITEM_STRING:
+ return push (ctx, item);
+
+ case ITEM_FLOAT:
+ value = strdup_printf ("%Lf", get_float (item));
+ break;
+ case ITEM_INTEGER:
+ value = strdup_printf ("%lld", get_integer (item));
+ break;
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_STRING));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ if (!value)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ item = new_string (value, -1);
+ free (value);
+ return push (ctx, item);
+}
+
+defn (fn_to_integer)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ long long value;
+
+ switch (item->type)
+ {
+ case ITEM_INTEGER:
+ return push (ctx, item);
+ case ITEM_FLOAT:
+ value = get_float (item);
+ break;
+
+ case ITEM_STRING:
+ {
+ char *end;
+ const char *s = get_string (item);
+ value = strtoll (s, &end, 10);
+ if (end != s && *s == '\0')
+ break;
+
+ item_free (item);
+ return set_error (ctx, "integer conversion error");
+ }
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_INTEGER));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ return push (ctx, new_integer (value));
+}
+
+defn (fn_to_float)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ long double value;
+
+ switch (item->type)
+ {
+ case ITEM_FLOAT:
+ return push (ctx, item);
+ case ITEM_INTEGER:
+ value = get_integer (item);
+ break;
+
+ case ITEM_STRING:
+ {
+ char *end;
+ const char *s = get_string (item);
+ value = strtold (s, &end);
+ if (end != s && *s == '\0')
+ break;
+
+ item_free (item);
+ return set_error (ctx, "float conversion error");
+ }
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_FLOAT));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ return push (ctx, new_float (value));
+}
+
+// - - Miscellaneous - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_length)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ bool success = true;
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ success = push (ctx, new_integer (((struct item_string *) item)->len));
+ break;
+ case ITEM_LIST:
+ {
+ long long length = 0;
+ struct item *iter;
+ for (iter = get_list (item); iter; iter = iter->next)
+ length++;
+ success = push (ctx, new_integer (length));
+ break;
+ }
+ default:
+ success = set_error (ctx, "invalid type");
+ }
+ item_free (item);
+ return success;
+}
+
+// - - Stack operations - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_dup)
+{
+ check_stack (1);
+ return push (ctx, new_clone (ctx->stack));
+}
+
+defn (fn_drop)
+{
+ check_stack (1);
+ item_free (pop (ctx));
+ return true;
+}
+
+defn (fn_swap)
+{
+ check_stack (2);
+ struct item *second = pop (ctx), *first = pop (ctx);
+ return push (ctx, second) && push (ctx, first);
+}
+
+defn (fn_call)
+{
+ check_stack (1);
+ struct item *script = pop (ctx);
+ bool success = check_type (ctx, script, ITEM_LIST)
+ && execute (ctx, get_list (script));
+ item_free (script);
+ return success;
+}
+
+defn (fn_dip)
+{
+ check_stack (2);
+ struct item *script = pop (ctx);
+ struct item *item = pop (ctx);
+ bool success = check_type (ctx, script, ITEM_LIST)
+ && execute (ctx, get_list (script));
+ item_free (script);
+ if (!success)
+ {
+ item_free (item);
+ return false;
+ }
+ return push (ctx, item);
+}
+
+defn (fn_unit)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ return push (ctx, new_list (item));
+}
+
+defn (fn_cons)
+{
+ check_stack (2);
+ struct item *list = pop (ctx);
+ struct item *item = pop (ctx);
+ if (!check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (list);
+ item_free (item);
+ return false;
+ }
+ item->next = get_list (list);
+ ((struct item_list *) list)->head = item;
+ return push (ctx, list);
+}
+
+defn (fn_cat)
+{
+ check_stack (2);
+ struct item *scnd = pop (ctx);
+ struct item *frst = pop (ctx);
+ if (!check_type (ctx, frst, ITEM_LIST)
+ || !check_type (ctx, scnd, ITEM_LIST))
+ {
+ item_free (frst);
+ item_free (scnd);
+ return false;
+ }
+
+ // XXX: we shouldn't have to do this in O(n)
+ struct item **tail = &((struct item_list *) frst)->head;
+ while (*tail)
+ tail = &(*tail)->next;
+ *tail = get_list (scnd);
+
+ ((struct item_list *) scnd)->head = NULL;
+ item_free (scnd);
+ return push (ctx, frst);
+}
+
+defn (fn_uncons)
+{
+ check_stack (1);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, list, ITEM_LIST))
+ goto fail;
+ struct item *first = get_list (list);
+ if (!first)
+ {
+ set_error (ctx, "list is empty");
+ goto fail;
+ }
+ ((struct item_list *) list)->head = first->next;
+ first->next = NULL;
+ return push (ctx, first) && push (ctx, list);
+fail:
+ item_free (list);
+ return false;
+}
+
+// - - Logical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+to_boolean (struct context *ctx, struct item *item, bool *ok)
+{
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ return *get_string (item) != '\0';
+ case ITEM_INTEGER:
+ return get_integer (item) != 0;
+ case ITEM_FLOAT:
+ return get_float (item) != 0.;
+ default:
+ return (*ok = set_error (ctx, "cannot convert `%s' to boolean",
+ item_type_to_str (item->type)));
+ }
+}
+
+defn (fn_not)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ bool ok = true;
+ bool result = !to_boolean (ctx, item, &ok);
+ item_free (item);
+ return ok && push (ctx, new_integer (result));
+}
+
+defn (fn_and)
+{
+ check_stack (2);
+ struct item *op1 = pop (ctx);
+ struct item *op2 = pop (ctx);
+ bool ok = true;
+ bool result = to_boolean (ctx, op1, &ok) && to_boolean (ctx, op2, &ok);
+ item_free (op1);
+ item_free (op2);
+ return ok && push (ctx, new_integer (result));
+}
+
+defn (fn_or)
+{
+ check_stack (2);
+ struct item *op1 = pop (ctx);
+ struct item *op2 = pop (ctx);
+ bool ok = true;
+ bool result = to_boolean (ctx, op1, &ok)
+ || !ok || to_boolean (ctx, op2, &ok);
+ item_free (op1);
+ item_free (op2);
+ return ok && push (ctx, new_integer (result));
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_if)
+{
+ check_stack (3);
+ struct item *else_ = pop (ctx);
+ struct item *then_ = pop (ctx);
+ struct item *cond_ = pop (ctx);
+
+ bool ok = true;
+ bool condition = to_boolean (ctx, cond_, &ok);
+ item_free (cond_);
+
+ bool success = false;
+ if (ok
+ && check_type (ctx, then_, ITEM_LIST)
+ && check_type (ctx, else_, ITEM_LIST))
+ success = execute (ctx, condition
+ ? get_list (then_)
+ : get_list (else_));
+
+ item_free (then_);
+ item_free (else_);
+ return success;
+}
+
+defn (fn_try)
+{
+ check_stack (2);
+ struct item *catch = pop (ctx);
+ struct item *try = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, try, ITEM_LIST)
+ || !check_type (ctx, catch, ITEM_LIST))
+ goto fail;
+
+ if (!execute (ctx, get_list (try)))
+ {
+ if (ctx->memory_failure || ctx->error_is_fatal)
+ goto fail;
+
+ success = push (ctx, new_string (ctx->error, -1));
+ free (ctx->error);
+ ctx->error = NULL;
+
+ if (success)
+ success = execute (ctx, get_list (catch));
+ }
+
+fail:
+ item_free (try);
+ item_free (catch);
+ return success;
+}
+
+defn (fn_map)
+{
+ check_stack (2);
+ struct item *fn = pop (ctx);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, fn, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (fn);
+ item_free (list);
+ return false;
+ }
+
+ bool success = false;
+ struct item *result = NULL, **tail = &result;
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ {
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (fn))
+ || !check_stack_safe (ctx, 1))
+ goto fail;
+
+ struct item *item = pop (ctx);
+ *tail = item;
+ tail = &item->next;
+ }
+ success = true;
+
+fail:
+ set_list (list, result);
+ item_free (fn);
+ if (!success)
+ {
+ item_free (list);
+ return false;
+ }
+ return push (ctx, list);
+}
+
+defn (fn_filter)
+{
+ check_stack (2);
+ struct item *fn = pop (ctx);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, fn, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (fn);
+ item_free (list);
+ return false;
+ }
+
+ bool success = false;
+ bool ok = true;
+ struct item *result = NULL, **tail = &result;
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ {
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (fn))
+ || !check_stack_safe (ctx, 1))
+ goto fail;
+
+ struct item *item = pop (ctx);
+ bool survived = to_boolean (ctx, item, &ok);
+ item_free (item);
+ if (!ok)
+ goto fail;
+ if (!survived)
+ continue;
+
+ if (!(item = new_clone (iter)))
+ goto fail;
+ *tail = item;
+ tail = &item->next;
+ }
+ success = true;
+
+fail:
+ set_list (list, result);
+ item_free (fn);
+ if (!success)
+ {
+ item_free (list);
+ return false;
+ }
+ return push (ctx, list);
+}
+
+defn (fn_fold)
+{
+ check_stack (3);
+ struct item *op = pop (ctx);
+ struct item *null = pop (ctx);
+ struct item *list = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, op, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (null);
+ goto fail;
+ }
+
+ push (ctx, null);
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (op)))
+ goto fail;
+ success = true;
+
+fail:
+ item_free (op);
+ item_free (list);
+ return success;
+}
+
+defn (fn_each)
+{
+ check_stack (2);
+ struct item *op = pop (ctx);
+ struct item *list = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, op, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ goto fail;
+
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (op)))
+ goto fail;
+ success = true;
+
+fail:
+ item_free (op);
+ item_free (list);
+ return success;
+}
+
+// - - Arithmetic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// XXX: why not a `struct item_string *` argument?
+static bool
+push_repeated_string (struct context *ctx, struct item *op1, struct item *op2)
+{
+ struct item_string *string = (struct item_string *) op1;
+ struct item_integer *repeat = (struct item_integer *) op2;
+ assert (string->type == ITEM_STRING);
+ assert (repeat->type == ITEM_INTEGER);
+
+ if (repeat->value < 0)
+ return set_error (ctx, "cannot multiply a string by a negative value");
+
+ char *buf = NULL;
+ size_t len = string->len * repeat->value;
+ if (len < string->len && repeat->value != 0)
+ goto allocation_fail;
+
+ buf = malloc (len);
+ if (!buf)
+ goto allocation_fail;
+
+ for (size_t i = 0; i < len; i += string->len)
+ memcpy (buf + i, string->value, string->len);
+ struct item *item = new_string (buf, len);
+ free (buf);
+ return push (ctx, item);
+
+allocation_fail:
+ ctx->memory_failure = true;
+ return false;
+}
+
+defn (fn_times)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) * get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) * get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) * get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) * get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_STRING)
+ ok = push_repeated_string (ctx, op2, op1);
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_INTEGER)
+ ok = push_repeated_string (ctx, op1, op2);
+ else
+ ok = set_error (ctx, "cannot multiply `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_pow)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ // TODO: implement this properly, outputting an integer
+ ok = push (ctx, new_float (powl (get_integer (op1), get_integer (op2))));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (powl (get_integer (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (powl (get_float (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (powl (get_float (op1), get_integer (op2))));
+ else
+ ok = set_error (ctx, "cannot exponentiate `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_div)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ {
+ if (get_integer (op2) == 0)
+ ok = set_error (ctx, "division by zero");
+ else
+ ok = push (ctx, new_integer (get_integer (op1) / get_integer (op2)));
+ }
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) / get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) / get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) / get_integer (op2)));
+ else
+ ok = set_error (ctx, "cannot divide `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_mod)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ {
+ if (get_integer (op2) == 0)
+ ok = set_error (ctx, "division by zero");
+ else
+ ok = push (ctx, new_integer (get_integer (op1) % get_integer (op2)));
+ }
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (fmodl (get_integer (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (fmodl (get_float (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (fmodl (get_float (op1), get_integer (op2))));
+ else
+ ok = set_error (ctx, "cannot divide `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+static bool
+push_concatenated_string (struct context *ctx,
+ struct item *op1, struct item *op2)
+{
+ struct item_string *s1 = (struct item_string *) op1;
+ struct item_string *s2 = (struct item_string *) op2;
+ assert (s1->type == ITEM_STRING);
+ assert (s2->type == ITEM_STRING);
+
+ char *buf = NULL;
+ size_t len = s1->len + s2->len;
+ if (len < s1->len || len < s2->len)
+ goto allocation_fail;
+
+ buf = malloc (len);
+ if (!buf)
+ goto allocation_fail;
+
+ memcpy (buf, s1->value, s1->len);
+ memcpy (buf + s1->len, s2->value, s2->len);
+ struct item *item = new_string (buf, len);
+ free (buf);
+ return push (ctx, item);
+
+allocation_fail:
+ ctx->memory_failure = true;
+ return false;
+
+}
+
+defn (fn_plus)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) + get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) + get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) + get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) + get_integer (op2)));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push_concatenated_string (ctx, op1, op2);
+ else
+ ok = set_error (ctx, "cannot add `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_minus)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) - get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) - get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) - get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) - get_integer (op2)));
+ else
+ ok = set_error (ctx, "cannot subtract `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+// - - Comparison - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+compare_strings (struct item_string *s1, struct item_string *s2)
+{
+ // XXX: not entirely correct wrt. null bytes
+ size_t len = (s1->len < s2->len ? s1->len : s2->len) + 1;
+ return memcmp (s1->value, s2->value, len);
+}
+
+static bool compare_lists (struct item *, struct item *);
+
+static bool
+compare_list_items (struct item *op1, struct item *op2)
+{
+ if (op1->type != op2->type)
+ return false;
+
+ switch (op1->type)
+ {
+ case ITEM_STRING:
+ case ITEM_WORD:
+ return !compare_strings ((struct item_string *) op1,
+ (struct item_string *) op2);
+ case ITEM_FLOAT:
+ return get_float (op1) == get_float (op2);
+ case ITEM_INTEGER:
+ return get_integer (op1) == get_integer (op2);
+ case ITEM_LIST:
+ return compare_lists (get_list (op1), get_list (op2));
+ }
+ abort ();
+}
+
+static bool
+compare_lists (struct item *op1, struct item *op2)
+{
+ while (op1 && op2)
+ {
+ if (!compare_list_items (op1, op2))
+ return false;
+
+ op1 = op1->next;
+ op2 = op2->next;
+ }
+ return !op1 && !op2;
+}
+
+defn (fn_eq)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) == get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_integer (op1) == get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_float (op1) == get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_float (op1) == get_integer (op2)));
+ else if (op1->type == ITEM_LIST && op2->type == ITEM_LIST)
+ ok = push (ctx, new_integer (compare_lists
+ (get_list (op1), get_list (op2))));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push (ctx, new_integer (compare_strings
+ ((struct item_string *)(op1), (struct item_string *)(op2)) == 0));
+ else
+ ok = set_error (ctx, "cannot compare `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_lt)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) < get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_integer (op1) < get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_float (op1) < get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_float (op1) < get_integer (op2)));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push (ctx, new_integer (compare_strings
+ ((struct item_string *)(op1), (struct item_string *)(op2)) < 0));
+ else
+ ok = set_error (ctx, "cannot compare `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+// - - Utilities - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_rand)
+{
+ return push (ctx, new_float ((long double) rand ()
+ / ((long double) RAND_MAX + 1)));
+}
+
+defn (fn_time)
+{
+ return push (ctx, new_integer (time (NULL)));
+}
+
+// XXX: this is a bit too constrained; combines strftime() with gmtime()
+defn (fn_strftime)
+{
+ check_stack (2);
+ struct item *format = pop (ctx);
+ struct item *time_ = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, time_, ITEM_INTEGER)
+ || !check_type (ctx, format, ITEM_STRING))
+ goto fail;
+
+ if (get_integer (time_) < 0)
+ {
+ set_error (ctx, "invalid time value");
+ goto fail;
+ }
+
+ char buf[128];
+ time_t time__ = get_integer (time_);
+ struct tm tm;
+ gmtime_r (&time__, &tm);
+ buf[strftime (buf, sizeof buf, get_string (format), &tm)] = '\0';
+ success = push (ctx, new_string (buf, -1));
+
+fail:
+ item_free (time_);
+ item_free (format);
+ return success;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void item_list_to_str (const struct item *, struct buffer *);
+
+static void
+string_to_str (const struct item_string *string, struct buffer *buf)
+{
+ buffer_append_c (buf, '"');
+ for (size_t i = 0; i < string->len; i++)
+ {
+ char c = string->value[i];
+ if (c == '\n') buffer_append (buf, "\\n", 2);
+ else if (c == '\r') buffer_append (buf, "\\r", 2);
+ else if (c == '\t') buffer_append (buf, "\\t", 2);
+ else if (!isprint (c))
+ {
+ char tmp[8];
+ snprintf (tmp, sizeof tmp, "\\x%02x", (unsigned char) c);
+ buffer_append (buf, tmp, strlen (tmp));
+ }
+ else if (c == '\\') buffer_append (buf, "\\\\", 2);
+ else if (c == '"') buffer_append (buf, "\\\"", 2);
+ else buffer_append_c (buf, c);
+ }
+ buffer_append_c (buf, '"');
+}
+
+static void
+item_to_str (const struct item *item, struct buffer *buf)
+{
+ switch (item->type)
+ {
+ char *x;
+ case ITEM_STRING:
+ string_to_str ((struct item_string *) item, buf);
+ break;
+ case ITEM_WORD:
+ {
+ struct item_word *word = (struct item_word *) item;
+ buffer_append (buf, word->value, word->len);
+ break;
+ }
+ case ITEM_INTEGER:
+ if (!(x = strdup_printf ("%lld", get_integer (item))))
+ goto alloc_failure;
+ buffer_append (buf, x, strlen (x));
+ free (x);
+ break;
+ case ITEM_FLOAT:
+ if (!(x = strdup_printf ("%Lf", get_float (item))))
+ goto alloc_failure;
+ buffer_append (buf, x, strlen (x));
+ free (x);
+ break;
+ case ITEM_LIST:
+ buffer_append_c (buf, '[');
+ item_list_to_str (get_list (item), buf);
+ buffer_append_c (buf, ']');
+ break;
+ }
+ return;
+
+alloc_failure:
+ // This is a bit hackish but it simplifies stuff
+ buf->memory_failure = true;
+ free (buf->s);
+ buf->s = NULL;
+}
+
+static void
+item_list_to_str (const struct item *script, struct buffer *buf)
+{
+ if (!script)
+ return;
+
+ item_to_str (script, buf);
+ while ((script = script->next))
+ {
+ buffer_append_c (buf, ' ');
+ item_to_str (script, buf);
+ }
+}
+
+// --- IRC protocol ------------------------------------------------------------
+
+struct message
+{
+ char *prefix; ///< Message prefix
+ char *command; ///< IRC command
+ char *params[16]; ///< Command parameters (0-terminated)
+ size_t n_params; ///< Number of parameters present
+};
+
+inline static char *
+cut_word (char **s)
+{
+ char *start = *s, *end = *s + strcspn (*s, " ");
+ *s = end + strspn (end, " ");
+ *end = '\0';
+ return start;
+}
+
+static bool
+parse_message (char *s, struct message *msg)
+{
+ memset (msg, 0, sizeof *msg);
+
+ // Ignore IRC 3.2 message tags, if present
+ if (*s == '@')
+ {
+ s += strcspn (s, " ");
+ s += strspn (s, " ");
+ }
+
+ // Prefix
+ if (*s == ':')
+ msg->prefix = cut_word (&s) + 1;
+
+ // Command
+ if (!*(msg->command = cut_word (&s)))
+ return false;
+
+ // Parameters
+ while (*s)
+ {
+ size_t n = msg->n_params++;
+ if (msg->n_params >= N_ELEMENTS (msg->params))
+ return false;
+ if (*s == ':')
+ {
+ msg->params[n] = ++s;
+ break;
+ }
+ msg->params[n] = cut_word (&s);
+ }
+ return true;
+}
+
+static struct message *
+read_message (void)
+{
+ static bool discard = false;
+ static char buf[1025];
+ static struct message msg;
+
+ bool discard_this;
+ do
+ {
+ if (!fgets (buf, sizeof buf, stdin))
+ return NULL;
+ size_t len = strlen (buf);
+
+ // Just to be on the safe side, if the line overflows our buffer,
+ // ignore everything up until the next line.
+ discard_this = discard;
+ if (len >= 2 && !strcmp (buf + len - 2, "\r\n"))
+ {
+ buf[len -= 2] = '\0';
+ discard = false;
+ }
+ else
+ discard = true;
+ }
+ // Invalid messages are silently ignored
+ while (discard_this || !parse_message (buf, &msg));
+ return &msg;
+}
+
+// --- Interfacing with the bot ------------------------------------------------
+
+#define BOT_PRINT "XB print :script: "
+
+static const char *
+get_config (const char *key)
+{
+ printf ("XB get_config :%s\r\n", key);
+ struct message *msg = read_message ();
+ if (!msg || msg->n_params <= 0)
+ exit (EXIT_FAILURE);
+ return msg->params[0];
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// TODO: implement more functions; try to avoid writing them in C
+
+static bool
+init_runtime_library_scripts (void)
+{
+ bool ok = true;
+
+ // It's much cheaper (and more fun) to define functions in terms of other
+ // ones. The "unit tests" serve a secondary purpose of showing the usage.
+ struct script
+ {
+ const char *name; ///< Name of the function
+ const char *definition; ///< The defining script
+ const char *unit_test; ///< Trivial unit test, must return 1
+ }
+ scripts[] =
+ {
+ { "nip", "swap drop", "1 2 nip 2 =" },
+ { "over", "[dup] dip swap", "1 2 over nip nip 1 =" },
+ { "swons", "swap cons", "[2] 1 swons [1 2] =" },
+ { "first", "uncons drop", "[1 2 3] first 1 =" },
+ { "rest", "uncons swap drop", "[1 2 3] rest [2 3] =" },
+ { "reverse", "[] swap [swap cons] each", "[1 2] reverse [2 1] =" },
+ { "curry", "cons", "1 2 [+] curry call 3 =" },
+
+ { "xor", "not swap not + 1 =", "1 1 xor 0 =" },
+ { "min", "over over < [drop] [nip] if", "1 2 min 1 =" },
+ { "max", "over over > [drop] [nip] if", "1 2 max 2 =" },
+
+ { "all?", "[and] cat 1 swap fold", "[3 4 5] [> 3] all? 0 =" },
+ { "any?", "[or] cat 0 swap fold", "[3 4 5] [> 3] any? 1 =" },
+
+ { ">", "swap <", "1 2 > 0 =" },
+ { "!=", "= not", "1 2 != 1 =" },
+ { "<=", "> not", "1 2 <= 1 =" },
+ { ">=", "< not", "1 2 >= 0 =" },
+
+ // XXX: this is a bit crazy and does not work with an empty list
+ { "join", "[uncons] dip swap [[dup] dip swap [+ +] dip] each drop",
+ "[1 2 3] [>string] map \" -> \" join \"1 -> 2 -> 3\" =" },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
+ {
+ const char *error = NULL;
+ struct item *script = parse (scripts[i].definition, &error);
+ if (error)
+ {
+ printf (BOT_PRINT "error parsing internal script `%s': %s\r\n",
+ scripts[i].definition, error);
+ ok = false;
+ }
+ else
+ ok &= register_script (scripts[i].name, script);
+ }
+
+ struct context ctx;
+ for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
+ {
+ const char *error = NULL;
+ struct item *script = parse (scripts[i].unit_test, &error);
+ if (error)
+ {
+ printf (BOT_PRINT "error parsing unit test for `%s': %s\r\n",
+ scripts[i].name, error);
+ ok = false;
+ continue;
+ }
+ context_init (&ctx);
+ execute (&ctx, script);
+ item_free_list (script);
+
+ const char *failure = NULL;
+ if (ctx.memory_failure)
+ failure = "memory allocation failure";
+ else if (ctx.error)
+ failure = ctx.error;
+ else if (ctx.stack_size != 1)
+ failure = "too many results on the stack";
+ else if (ctx.stack->type != ITEM_INTEGER)
+ failure = "result is not an integer";
+ else if (get_integer (ctx.stack) != 1)
+ failure = "wrong test result";
+ if (failure)
+ {
+ printf (BOT_PRINT "error executing unit test for `%s': %s\r\n",
+ scripts[i].name, failure);
+ ok = false;
+ }
+ context_free (&ctx);
+ }
+ return ok;
+}
+
+static bool
+init_runtime_library (void)
+{
+ bool ok = true;
+
+ // Type detection
+ ok &= register_handler ("string?", fn_is_string);
+ ok &= register_handler ("word?", fn_is_word);
+ ok &= register_handler ("integer?", fn_is_integer);
+ ok &= register_handler ("float?", fn_is_float);
+ ok &= register_handler ("list?", fn_is_list);
+
+ // Type conversion
+ ok &= register_handler (">string", fn_to_string);
+ ok &= register_handler (">integer", fn_to_integer);
+ ok &= register_handler (">float", fn_to_float);
+
+ // Miscellaneous
+ ok &= register_handler ("length", fn_length);
+
+ // Basic stack manipulation
+ ok &= register_handler ("dup", fn_dup);
+ ok &= register_handler ("drop", fn_drop);
+ ok &= register_handler ("swap", fn_swap);
+
+ // Calling stuff
+ ok &= register_handler ("call", fn_call);
+ ok &= register_handler ("dip", fn_dip);
+
+ // Control flow
+ ok &= register_handler ("if", fn_if);
+ ok &= register_handler ("try", fn_try);
+
+ // List processing
+ ok &= register_handler ("map", fn_map);
+ ok &= register_handler ("filter", fn_filter);
+ ok &= register_handler ("fold", fn_fold);
+ ok &= register_handler ("each", fn_each);
+
+ // List manipulation
+ ok &= register_handler ("unit", fn_unit);
+ ok &= register_handler ("cons", fn_cons);
+ ok &= register_handler ("cat", fn_cat);
+ ok &= register_handler ("uncons", fn_uncons);
+
+ // Arithmetic operations
+ ok &= register_handler ("+", fn_plus);
+ ok &= register_handler ("-", fn_minus);
+ ok &= register_handler ("*", fn_times);
+ ok &= register_handler ("^", fn_pow);
+ ok &= register_handler ("/", fn_div);
+ ok &= register_handler ("%", fn_mod);
+
+ // Comparison
+ ok &= register_handler ("=", fn_eq);
+ ok &= register_handler ("<", fn_lt);
+
+ // Logical operations
+ ok &= register_handler ("not", fn_not);
+ ok &= register_handler ("and", fn_and);
+ ok &= register_handler ("or", fn_or);
+
+ // Utilities
+ ok &= register_handler ("rand", fn_rand);
+ ok &= register_handler ("time", fn_time);
+ ok &= register_handler ("strftime", fn_strftime);
+
+ ok &= init_runtime_library_scripts ();
+ return ok;
+}
+
+static void
+free_runtime_library (void)
+{
+ struct fn *next, *iter;
+ for (iter = g_functions; iter; iter = next)
+ {
+ next = iter->next;
+ free_function (iter);
+ }
+}
+
+// --- Function database -------------------------------------------------------
+
+// TODO: a global variable storing the various procedures (db)
+// XXX: defining procedures would ideally need some kind of an ACL
+
+static void
+read_db (void)
+{
+ // TODO
+}
+
+static void
+write_db (void)
+{
+ // TODO
+}
+
+// --- Main --------------------------------------------------------------------
+
+static char *g_prefix;
+
+struct user_info
+{
+ char *ctx; ///< Context: channel or user
+ char *ctx_quote; ///< Reply quotation
+};
+
+defn (fn_dot)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ struct user_info *info = ctx->user_data;
+
+ struct buffer buf = BUFFER_INITIALIZER;
+ item_to_str (item, &buf);
+ item_free (item);
+ buffer_append_c (&buf, '\0');
+ if (buf.memory_failure)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ if (buf.len > 255)
+ buf.s[255] = '\0';
+
+ printf ("PRIVMSG %s :%s%s\r\n", info->ctx, info->ctx_quote, buf.s);
+ free (buf.s);
+ return true;
+}
+
+static void
+process_message (struct message *msg)
+{
+ if (!msg->prefix
+ || strcasecmp (msg->command, "PRIVMSG")
+ || msg->n_params < 2)
+ return;
+ char *line = msg->params[1];
+
+ // Filter out only our commands
+ size_t prefix_len = strlen (g_prefix);
+ if (strncmp (line, g_prefix, prefix_len))
+ return;
+ line += prefix_len;
+
+ char *command = cut_word (&line);
+ if (strcasecmp (command, "script"))
+ return;
+
+ // Retrieve information on how to respond back
+ char *msg_ctx = msg->prefix, *x;
+ if ((x = strchr (msg_ctx, '!')))
+ *x = '\0';
+
+ char *msg_ctx_quote;
+ if (strchr ("#+&!", *msg->params[0]))
+ {
+ msg_ctx_quote = strdup_printf ("%s: ", msg_ctx);
+ msg_ctx = msg->params[0];
+ }
+ else
+ msg_ctx_quote = strdup ("");
+
+ if (!msg_ctx_quote)
+ {
+ printf (BOT_PRINT "%s\r\n", "memory allocation failure");
+ return;
+ }
+
+ struct user_info info;
+ info.ctx = msg_ctx;
+ info.ctx_quote = msg_ctx_quote;
+
+ // Finally parse and execute the macro
+ const char *error = NULL;
+ struct item *script = parse (line, &error);
+ if (error)
+ {
+ printf ("PRIVMSG %s :%s%s: %s\r\n",
+ msg_ctx, msg_ctx_quote, "parse error", error);
+ goto end;
+ }
+
+ struct context ctx;
+ context_init (&ctx);
+ ctx.user_data = &info;
+ execute (&ctx, script);
+ item_free_list (script);
+
+ const char *failure = NULL;
+ if (ctx.memory_failure)
+ failure = "memory allocation failure";
+ else if (ctx.error)
+ failure = ctx.error;
+ if (failure)
+ printf ("PRIVMSG %s :%s%s: %s\r\n",
+ msg_ctx, msg_ctx_quote, "runtime error", failure);
+ context_free (&ctx);
+end:
+ free (msg_ctx_quote);
+}
+
+int
+main (int argc, char *argv[])
+{
+ freopen (NULL, "rb", stdin); setvbuf (stdin, NULL, _IOLBF, BUFSIZ);
+ freopen (NULL, "wb", stdout); setvbuf (stdout, NULL, _IOLBF, BUFSIZ);
+
+ struct rlimit limit =
+ {
+ .rlim_cur = ADDRESS_SPACE_LIMIT,
+ .rlim_max = ADDRESS_SPACE_LIMIT
+ };
+
+ // Lower the memory limits to something sensible to prevent abuse
+ (void) setrlimit (RLIMIT_AS, &limit);
+
+ read_db ();
+ if (!init_runtime_library ()
+ || !register_handler (".", fn_dot))
+ printf (BOT_PRINT "%s\r\n", "runtime library initialization failed");
+
+ g_prefix = strdup (get_config ("prefix"));
+ printf ("XB register\r\n");
+ struct message *msg;
+ while ((msg = read_message ()))
+ process_message (msg);
+
+ free_runtime_library ();
+ free (g_prefix);
+ return 0;
+}
+
diff --git a/plugins/xB/seen b/plugins/xB/seen
new file mode 100755
index 0000000..da43488
--- /dev/null
+++ b/plugins/xB/seen
@@ -0,0 +1,160 @@
+#!/usr/bin/env lua
+--
+-- xB seen plugin
+--
+-- Copyright 2016 Přemysl Eric Janouch <p@janouch.name>
+-- See the file LICENSE for licensing information.
+--
+
+function parse (line)
+ local msg = { params = {} }
+ line = line:match ("[^\r]*")
+ for start, word in line:gmatch ("()([^ ]+)") do
+ local colon = word:match ("^:(.*)")
+ if start == 1 and colon then
+ msg.prefix = colon
+ elseif not msg.command then
+ msg.command = word
+ elseif colon then
+ table.insert (msg.params, line:sub (start + 1))
+ break
+ elseif start ~= #line then
+ table.insert (msg.params, word)
+ end
+ end
+ return msg
+end
+
+function get_config (name)
+ io.write ("XB get_config :", name, "\r\n")
+ return parse (io.read ()).params[1]
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+io.output ():setvbuf ('line')
+local prefix = get_config ('prefix')
+io.write ("XB register\r\n")
+
+local db = {}
+local db_filename = "seen.db"
+local db_garbage = 0
+
+function remember (who, where, when, what)
+ if not db[who] then db[who] = {} end
+ if db[who][where] then db_garbage = db_garbage + 1 end
+ db[who][where] = { tonumber (when), what }
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local db_file, e = io.open (db_filename, "a+")
+if not db_file then error ("cannot open database: " .. e, 0) end
+
+function db_store (who, where, when, what)
+ db_file:write (string.format
+ (":%s %s %s %s :%s\n", who, "PRIVMSG", where, when, what))
+end
+
+function db_compact ()
+ db_file:close ()
+
+ -- Unfortunately, default Lua doesn't have anything like mkstemp()
+ local db_tmpname = db_filename .. "." .. os.time ()
+ db_file, e = io.open (db_tmpname, "a+")
+ if not db_file then error ("cannot save database: " .. e, 0) end
+
+ for who, places in pairs (db) do
+ for where, data in pairs (places) do
+ db_store (who, where, data[1], data[2])
+ end
+ end
+ db_file:flush ()
+
+ local ok, e = os.rename (db_tmpname, db_filename)
+ if not ok then error ("cannot save database: " .. e, 0) end
+ db_garbage = 0
+end
+
+for line in db_file:lines () do
+ local msg = parse (line)
+ remember (msg.prefix, table.unpack (msg.params))
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+function seen (who, where, args)
+ local respond = function (...)
+ local privmsg = function (target, ...)
+ io.write ("PRIVMSG ", target, " :", table.concat { ... }, "\r\n")
+ end
+ if where:match ("^[#&!+]") then
+ privmsg (where, who, ": ", ...)
+ else
+ privmsg (who, ...)
+ end
+ end
+
+ local whom, e, garbage = args:match ("^(%S+)()%s*(.*)")
+ if not whom or #garbage ~= 0 then
+ return respond ("usage: <name>")
+ elseif who:lower () == whom:lower () then
+ return respond ("I can see you right now.")
+ end
+
+ local top = {}
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ local pattern = "^" .. whom:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*"):lower () .. "$"
+ for name, places in pairs (db) do
+ if places[where] and name:lower ():match (pattern) then
+ local when, what = table.unpack (places[where])
+ table.insert (top, { name = name, when = when, what = what })
+ end
+ end
+ if #top == 0 then
+ return respond ("I have not seen \x02" .. whom .. "\x02 here.")
+ end
+
+ -- Get all matching nicknames ordered from the most recently active
+ -- and make the list case insensitive (remove older duplicates)
+ table.sort (top, function (a, b) return a.when > b.when end)
+ for i = #top, 2, -1 do
+ if top[i - 1].name:lower () == top[i].name:lower () then
+ table.remove (top, i)
+ end
+ end
+
+ -- Hopefully the formatting mess will disrupt highlights in clients
+ for i = 1, math.min (#top, 3) do
+ local name = top[i].name:gsub ("^.", "%0\x02\x02")
+ respond (string.format ("\x02%s\x02 -> %s -> %s",
+ name, os.date ("%c", top[i].when), top[i].what))
+ end
+end
+
+function handle (msg)
+ local who = msg.prefix:match ("^[^!@]*")
+ local where, what = table.unpack (msg.params)
+ local when = os.time ()
+
+ local what_log = what:gsub ("^\x01ACTION", "*"):gsub ("\x01$", "")
+ remember (who, where, when, what_log)
+ db_store (who, where, when, what_log)
+
+ -- Comment out to reduce both disk load and reliability
+ db_file:flush ()
+
+ if db_garbage > 5000 then db_compact () end
+
+ if what:sub (1, #prefix) == prefix then
+ local command = what:sub (#prefix + 1)
+ local name, e = command:match ("^(%S+)%s*()")
+ if name == 'seen' then seen (who, where, command:sub (e)) end
+ end
+end
+
+for line in io.lines () do
+ local msg = parse (line)
+ if msg.command == "PRIVMSG" then handle (msg) end
+end
diff --git a/plugins/xB/seen-import-xC.pl b/plugins/xB/seen-import-xC.pl
new file mode 100755
index 0000000..db706a0
--- /dev/null
+++ b/plugins/xB/seen-import-xC.pl
@@ -0,0 +1,39 @@
+#!/usr/bin/env perl
+# Creates a database for the "seen" plugin from logs for xC.
+# The results may not be completely accurate but are good for jumpstarting.
+# Usage: ./seen-import-xC.pl LOG-FILE... > seen.db
+
+use strict;
+use warnings;
+use File::Basename;
+use Time::Piece;
+
+my $db = {};
+for (@ARGV) {
+ my $where = (basename($_) =~ /\.(.*).log/)[0];
+ unless ($where) {
+ print STDERR "Invalid filename: $_\n";
+ next;
+ }
+
+ open my $fh, '<', $_ or die "Failed to open log file: $!";
+ while (<$fh>) {
+ my ($when, $who, $who_action, $what) =
+ /^(.{19}) (?:<[~&@%+]*(.*?)>| \* (\S+)) (.*)/;
+ next unless $when;
+
+ if ($who_action) {
+ $who = $who_action;
+ $what = "* $what";
+ }
+ $db->{$who}->{$where} =
+ [Time::Piece->strptime($when, "%Y-%m-%d %T")->epoch, $what];
+ }
+}
+
+while (my ($who, $places) = each %$db) {
+ while (my ($where, $data) = each %$places) {
+ my ($when, $what) = @$data;
+ print ":$who PRIVMSG $where $when :$what\n";
+ }
+}
diff --git a/plugins/xB/youtube b/plugins/xB/youtube
new file mode 100755
index 0000000..feaab96
--- /dev/null
+++ b/plugins/xB/youtube
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# xB YouTube plugin, displaying info about YouTube links
+#
+# Copyright 2014 - 2015, Přemysl Eric Janouch <p@janouch.name>
+# See the file LICENSE for licensing information.
+#
+
+import sys
+import io
+import re
+import json
+import urllib.request
+
+class Plugin:
+ re_msg = re.compile ('(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?'
+ '([^ ]+)(?: +(.*))?\r\n$')
+ re_args = re.compile (':?((?<=:).*|[^ ]+) *')
+
+ def parse (self, line):
+ m = self.re_msg.match (line)
+ if m is None:
+ return None
+
+ (nick, user, host, command, args) = m.groups ()
+ args = [] if args is None else self.re_args.findall (args)
+ return (nick, user, host, command, args)
+
+ def get_config (self, key):
+ print ("XB get_config :%s" % key)
+ (_, _, _, _, args) = self.parse (sys.stdin.readline ())
+ return args[0]
+
+ def bot_print (self, what):
+ print ('XB print :%s' % what)
+
+class YouTube (Plugin):
+ re_videos = [re.compile (x) for x in [
+ r'youtube\.[a-z]+/[^ ]*[&?]v=([-\w]+)',
+ r'youtube\.[a-z]+/v/([-\w]+)',
+ r'youtu\.be/([-\w]+)'
+ ]]
+ re_playlists = [re.compile (x) for x in [
+ r'youtube\.[a-z]+/playlist[&?][^ ]*(?<=&|\?)list=([-\w]+)',
+ ]]
+
+ def print_info (self, channel, url, cb):
+ try:
+ data = json.loads (urllib.request.urlopen
+ (url, None, 30).read ().decode ('utf-8'))
+
+ for line in map (lambda x: "YouTube: " + cb (x), data['items']):
+ print ("PRIVMSG %s :%s" % (channel,
+ line.encode ('utf-8').decode ('iso8859-1')))
+
+ except Exception as err:
+ self.bot_print ('youtube: %s' % (err))
+
+ def print_video_info (self, channel, video_id):
+ url = 'https://www.googleapis.com/youtube/v3/' \
+ + 'videos?id=%s&key=%s&part=snippet,contentDetails,statistics' \
+ % (video_id, self.youtube_api_key)
+ self.print_info (channel, url, lambda x: "%s | %s | %sx" % (
+ x['snippet']['title'],
+ x['contentDetails']['duration'][2:].lower (),
+ x['statistics']['viewCount']))
+
+ def print_playlist_info (self, channel, playlist_id):
+ url = 'https://www.googleapis.com/youtube/v3/' \
+ + 'playlists?id=%s&key=%s&part=snippet,contentDetails' \
+ % (playlist_id, self.youtube_api_key)
+ self.print_info (channel, url, lambda x: "%s | %d videos" % (
+ x['snippet']['title'],
+ x['contentDetails']['itemCount']))
+
+ def process_line (self, line):
+ msg = self.parse (line)
+ if msg is None:
+ return
+
+ (nick, user, host, command, args) = msg
+ if command != 'PRIVMSG' or len (args) < 2:
+ return
+
+ ctx = args[0]
+ if not ctx.startswith (('#', '+', '&', '!')):
+ ctx = nick
+
+ for regex in self.re_videos:
+ for i in regex.findall (args[1]):
+ self.print_video_info (ctx, i)
+ for regex in self.re_playlists:
+ for i in regex.findall (args[1]):
+ self.print_playlist_info (ctx, i)
+
+ def run (self):
+ self.youtube_api_key = self.get_config ('youtube_api_key')
+ if self.youtube_api_key == "":
+ self.bot_print ("youtube: missing `youtube_api_key'")
+
+ print ("XB register")
+
+ for line in sys.stdin:
+ self.process_line (line)
+
+sys.stdin = io.TextIOWrapper (sys.__stdin__.buffer,
+ encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
+sys.stdout = io.TextIOWrapper (sys.__stdout__.buffer,
+ encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
+
+YouTube ().run ()
diff --git a/plugins/xC/auto-rejoin.lua b/plugins/xC/auto-rejoin.lua
new file mode 100644
index 0000000..f42fb2e
--- /dev/null
+++ b/plugins/xC/auto-rejoin.lua
@@ -0,0 +1,48 @@
+--
+-- auto-rejoin.lua: join back automatically when someone kicks you
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+local timeout
+xC.setup_config {
+ timeout = {
+ type = "integer",
+ comment = "auto rejoin timeout",
+ default = "0",
+
+ on_change = function (v)
+ timeout = v
+ end,
+ validate = function (v)
+ if v < 0 then error ("timeout must not be negative", 0) end
+ end,
+ },
+}
+
+async, await = xC.async, coroutine.yield
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "KICK" then return line end
+
+ local who = msg.prefix:match ("^[^!]*")
+ local channel, whom = table.unpack (msg.params)
+ if who ~= whom and whom == server.user.nickname then
+ async.go (function ()
+ await (async.timer_ms (timeout * 1000))
+ server:send ("JOIN " .. channel)
+ end)
+ end
+ return line
+end)
diff --git a/plugins/xC/censor.lua b/plugins/xC/censor.lua
new file mode 100644
index 0000000..49cab5b
--- /dev/null
+++ b/plugins/xC/censor.lua
@@ -0,0 +1,90 @@
+--
+-- censor.lua: black out certain users' messages
+--
+-- Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+local to_pattern = function (mask)
+ if not mask:match ("!") then mask = mask .. "!*" end
+ if not mask:match ("@") then mask = mask .. "@*" end
+
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ return "^" .. mask:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*") .. "$"
+end
+
+local patterns = {}
+local read_masks = function (v)
+ patterns = {}
+ local add = function (who, where)
+ local channels = patterns[who] or {}
+ table.insert (channels, where)
+ patterns[who] = channels
+ end
+ for item in v:lower ():gmatch ("[^,]+") do
+ local who, where = item:match ("^([^/]+)/*(.*)")
+ if who then add (to_pattern (who), where == "" or where) end
+ end
+end
+
+local quote
+xC.setup_config {
+ masks = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "user masks (optionally \"/#channel\") to censor",
+ on_change = read_masks
+ },
+ quote = {
+ type = "string",
+ default = "\"\\x0301,01\"",
+ comment = "formatting prefix for censored messages",
+ on_change = function (v) quote = v end
+ },
+}
+
+local decolor = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, text:sub (last, start - 1))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ end
+ return table.concat (rebuilt) .. text:sub (last)
+end
+
+local censor = function (line)
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ local ctcp, rest = text:match ("^(\x01%g+ )(.*)")
+ text = ctcp and ctcp .. quote .. decolor (rest) or quote .. decolor (text)
+ return start .. text
+end
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "PRIVMSG" then return line end
+
+ local channel = msg.params[1]:lower ()
+ for who, where in pairs (patterns) do
+ if msg.prefix:lower ():match (who) then
+ for _, x in pairs (where) do
+ if x == true or x == channel then
+ return censor (line)
+ end
+ end
+ end
+ end
+ return line
+end)
diff --git a/plugins/xC/fancy-prompt.lua b/plugins/xC/fancy-prompt.lua
new file mode 100644
index 0000000..0b7000c
--- /dev/null
+++ b/plugins/xC/fancy-prompt.lua
@@ -0,0 +1,113 @@
+--
+-- fancy-prompt.lua: the fancy multiline prompt you probably want
+--
+-- Copyright (c) 2016 - 2022, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+-- Beware that it is a hack and only goes about 90% of the way, which is why
+-- this functionality is only available as a plugin in the first place
+-- (well, and also for customizability).
+--
+-- The biggest problem is that the way we work with Readline is incompatible
+-- with multiline prompts, and normal newlines just don't work. This is being
+-- circumvented by using an overflowing single-line prompt with a specially
+-- crafted character in the rightmost column that prevents the bar's background
+-- from spilling all over the last line.
+--
+-- There is also a problem with C-r search rendering not clearing out the
+-- background but to really fix that mode, we'd have to fully reimplement it
+-- since its alternative prompt very often gets overriden by accident anyway.
+
+xC.hook_prompt (function (hook)
+ local current = xC.current_buffer
+ local chan = current.channel
+ local s = current.server
+
+ local bg_color = "255"
+ local current_n = 0
+ local active = ""
+ for i, buffer in ipairs (xC.buffers) do
+ if buffer == current then
+ current_n = i
+ elseif buffer.new_messages_count ~= buffer.new_unimportant_count then
+ active = active .. ","
+ if buffer.highlighted then
+ active = active .. "!"
+ bg_color = "224"
+ end
+ active = active .. i
+ end
+ end
+ local x = current_n .. ":" .. current.name
+ if chan and chan.users_len ~= 0 then
+ local params = ""
+ for mode, param in pairs (chan.param_modes) do
+ params = params .. " +" .. mode .. " " .. param
+ end
+ local modes = chan.no_param_modes .. params:sub (3)
+ if modes ~= "" then
+ x = x .. "(+" .. modes .. ")"
+ end
+ x = x .. "{" .. chan.users_len .. "}"
+ end
+ if current.hide_unimportant then
+ x = x .. "<H>"
+ end
+ if active ~= "" then
+ x = x .. " (" .. active:sub (2) .. ")"
+ end
+
+ -- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
+ -- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
+ --x = x:gsub("[\128-\255]", "?")
+
+ -- Align to the terminal's width and apply formatting, including the hack.
+ local lines, cols = xC.get_screen_size ()
+ local trailing, width = " ", xC.measure (x)
+ while cols > 0 and width >= cols do
+ x = x:sub (1, utf8.offset (x, -1) - 1)
+ trailing, width = ">", xC.measure (x)
+ end
+
+ x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
+ x .. string.rep (" ", cols - width - 1) ..
+ "\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02" ..
+ trailing .. "\x01\x1b[0;1m\x02"
+
+ local user_prefix = function (chan, user)
+ for i, chan_user in ipairs (chan.users) do
+ if chan_user.user == user then return chan_user.prefixes end
+ end
+ return ""
+ end
+ if s then
+ x = x .. "["
+ local state = s.state
+ if state == "disconnected" or state == "connecting" then
+ x = x .. "(" .. state .. ")"
+ elseif state ~= "registered" then
+ x = x .. "(unregistered)"
+ else
+ local user, modes = s.user, s.user_mode
+ if chan then x = x .. user_prefix (chan, user) end
+ x = x .. user.nickname
+ if modes ~= "" then x = x .. "(" .. modes .. ")" end
+ end
+ x = x .. "] "
+ else
+ -- There needs to be at least one character so that the cursor
+ -- doesn't get damaged by our hack in that last column
+ x = x .. "> "
+ end
+ return x
+end)
diff --git a/plugins/xC/last-fm.lua b/plugins/xC/last-fm.lua
new file mode 100644
index 0000000..3bdfed2
--- /dev/null
+++ b/plugins/xC/last-fm.lua
@@ -0,0 +1,178 @@
+--
+-- last-fm.lua: "now playing" feature using the last.fm API
+--
+-- Dependencies: lua-cjson (from luarocks e.g.)
+--
+-- I call this style closure-oriented programming
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+local cjson = require "cjson"
+
+-- Setup configuration to load last.fm API credentials from
+local user, api_key
+xC.setup_config {
+ user = {
+ type = "string",
+ comment = "last.fm username",
+ on_change = function (v) user = v end
+ },
+ api_key = {
+ type = "string",
+ comment = "last.fm API key",
+ on_change = function (v) api_key = v end
+ },
+}
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Generic error reporting
+local report_error = function (buffer, error)
+ buffer:log ("last-fm error: " .. error)
+end
+
+-- Process data return by the server and extract the now playing song
+local process = function (buffer, data, action)
+ -- There's no reasonable Lua package to parse HTTP that I could find
+ local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
+ if not s then return "server returned unexpected data" end
+ if status ~= "200" then return status .. " " .. message end
+
+ local s, e = string.find (data, "\r\n\r\n")
+ if not s then return "server returned unexpected data" end
+
+ local parser = cjson.new ()
+ data = parser.decode (string.sub (data, e + 1))
+ if not data.recenttracks or not data.recenttracks.track then
+ return "invalid response" end
+
+ -- Need to make some sense of the XML automatically converted to JSON
+ local text_of = function (node)
+ if type (node) ~= "table" then return node end
+ return node["#text"] ~= "" and node["#text"] or nil
+ end
+
+ local name, artist, album
+ for i, track in ipairs (data.recenttracks.track) do
+ if track["@attr"] and track["@attr"].nowplaying then
+ if track.name then name = text_of (track.name) end
+ if track.artist then artist = text_of (track.artist) end
+ if track.album then album = text_of (track.album) end
+ end
+ end
+
+ if not name then
+ action (false)
+ else
+ local np = "\"" .. name .. "\""
+ if artist then np = np .. " by " .. artist end
+ if album then np = np .. " from " .. album end
+ action (np)
+ end
+end
+
+-- Set up the connection and make the request
+local on_connected = function (buffer, c, host, action)
+ -- Buffer data in the connection object
+ c.data = ""
+ c.on_data = function (data)
+ c.data = c.data .. data
+ end
+
+ -- And process it after we receive everything
+ c.on_eof = function ()
+ error = process (buffer, c.data, action)
+ if error then report_error (buffer, error) end
+ c:close ()
+ end
+ c.on_error = function (e)
+ report_error (buffer, e)
+ end
+
+ -- Make the unencrypted HTTP request
+ local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
+ "&limit=1&api_key=" .. api_key .. "&format=json"
+ c:send ("GET " .. url .. " HTTP/1.1\r\n")
+ c:send ("User-agent: last-fm.lua\r\n")
+ c:send ("Host: " .. host .. "\r\n")
+ c:send ("Connection: close\r\n")
+ c:send ("\r\n")
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Avoid establishing more than one connection at a time
+local running
+
+-- Initiate a connection to last.fm servers
+async, await = xC.async, coroutine.yield
+local make_request = function (buffer, action)
+ if not user or not api_key then
+ report_error (buffer, "configuration is incomplete")
+ return
+ end
+
+ if running then running:cancel () end
+ running = async.go (function ()
+ local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80))
+ if e then
+ report_error (buffer, e)
+ else
+ on_connected (buffer, c, host, action)
+ end
+ running = nil
+ end)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local now_playing
+
+local tell_song = function (buffer)
+ if now_playing == nil then
+ buffer:log ("last-fm: I don't know what you're listening to")
+ elseif not now_playing then
+ buffer:log ("last-fm: not playing anything right now")
+ else
+ buffer:log ("last-fm: now playing: " .. now_playing)
+ end
+end
+
+local send_song = function (buffer)
+ if not now_playing then
+ tell_song (buffer)
+ else
+ buffer:execute ("/me is listening to " .. now_playing)
+ end
+end
+
+-- Hook input to simulate new commands
+xC.hook_input (function (hook, buffer, input)
+ if input == "/np" then
+ make_request (buffer, function (np)
+ now_playing = np
+ send_song (buffer)
+ end)
+ elseif input == "/np?" then
+ make_request (buffer, function (np)
+ now_playing = np
+ tell_song (buffer)
+ end)
+ elseif input == "/np!" then
+ send_song (buffer)
+ else
+ return input
+ end
+end)
diff --git a/plugins/xC/ping-timeout.lua b/plugins/xC/ping-timeout.lua
new file mode 100644
index 0000000..c455c57
--- /dev/null
+++ b/plugins/xC/ping-timeout.lua
@@ -0,0 +1,32 @@
+--
+-- ping-timeout.lua: ping timeout readability enhancement plugin
+--
+-- Copyright (c) 2015 - 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ local start, timeout = line:match ("^(.* :Ping timeout:) (%d+) seconds$")
+ if msg.command ~= "QUIT" or not start then
+ return line
+ end
+
+ local minutes = timeout // 60
+ if minutes == 0 then
+ return line
+ end
+
+ local seconds = timeout % 60
+ return ("%s %d minutes, %d seconds"):format (start, minutes, seconds)
+end)
diff --git a/plugins/xC/prime.lua b/plugins/xC/prime.lua
new file mode 100644
index 0000000..23740ee
--- /dev/null
+++ b/plugins/xC/prime.lua
@@ -0,0 +1,68 @@
+--
+-- prime.lua: highlight prime numbers in messages
+--
+-- Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+local smallest, highlight = 0, "\x1f"
+xC.setup_config {
+ smallest = {
+ type = "integer",
+ default = "0",
+ comment = "smallest number to scan for primality",
+ on_change = function (v) smallest = math.max (v, 2) end
+ },
+ highlight = {
+ type = "string",
+ default = "\"\\x1f\"",
+ comment = "the attribute to use for highlights",
+ on_change = function (v) highlight = v end
+ },
+}
+
+-- The prime test is actually very fast, so there is no DoS concern
+local do_intercolour = function (text)
+ return tostring (text:gsub ("%f[%w_]%d+", function (n)
+ if tonumber (n) < smallest then return nil end
+ for i = 2, n ^ (1 / 2) do if (n % i) == 0 then return nil end end
+ return highlight .. n .. highlight
+ end))
+end
+
+local do_interlink = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, do_intercolour (text:sub (last, start - 1)))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ table.insert (rebuilt, text:sub (start, last - 1))
+ end
+ return table.concat (rebuilt) .. do_intercolour (text:sub (last))
+end
+
+local do_message = function (text)
+ local rebuilt, last = {""}, 1
+ for run, link, endpos in text:gmatch ('(.-)(%f[%g]https?://%g+)()') do
+ last = endpos
+ table.insert (rebuilt, do_interlink (run) .. link)
+ end
+ return table.concat (rebuilt) .. do_interlink (text:sub (last))
+end
+
+-- XXX: sadly it won't typically highlight primes in our own messages,
+-- unless IRCv3 echo-message is on
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ return message and start .. do_message (message) or line
+end)
diff --git a/plugins/xC/slack.lua b/plugins/xC/slack.lua
new file mode 100644
index 0000000..c1a08de
--- /dev/null
+++ b/plugins/xC/slack.lua
@@ -0,0 +1,147 @@
+--
+-- slack.lua: try to fix up UX when using the Slack IRC gateway
+--
+-- Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+local servers = {}
+local read_servers = function (v)
+ servers = {}
+ for name in v:lower ():gmatch "[^,]+" do
+ servers[name] = true
+ end
+end
+
+-- This is a reverse list of Slack's automatic emoji, noseless forms
+local unemojify, emoji, emoji_default = false, {}, {
+ heart = "<3",
+ broken_heart = "</3",
+ sunglasses = "8)",
+ anguished = "D:",
+ cry = ":'(",
+ monkey_face = ":o)",
+ kiss = ":*",
+ smiley = "=)",
+ smile = ":D",
+ wink = ";)",
+ laughing = ":>",
+ neutral_face = ":|",
+ open_mouth = ":o",
+ angry = ">:(",
+ slightly_smiling_face = ":)",
+ disappointed = ":(",
+ confused = ":/",
+ stuck_out_tongue = ":p",
+ stuck_out_tongue_winking_eye = ";p",
+}
+local load_emoji = function (extra)
+ emoji = {}
+ for k, v in pairs (emoji_default) do emoji[k] = v end
+ for k, v in extra:gmatch "([^,]+) ([^,]+)" do emoji[k] = v end
+end
+
+xC.setup_config {
+ servers = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "list of server names that are Slack IRC gateways",
+ on_change = read_servers
+ },
+ unemojify = {
+ type = "boolean",
+ default = "true",
+ comment = "convert emoji to normal ASCII emoticons",
+ on_change = function (v) unemojify = v end
+ },
+ extra_emoji = {
+ type = "string_array",
+ default = "\"grinning :)),joy :'),innocent o:),persevere >_<\"",
+ comment = "overrides or extra emoji for unemojify",
+ on_change = function (v) load_emoji (v) end
+ }
+}
+
+-- We can handle external messages about what we've supposedly sent just fine,
+-- so let's get rid of that "[username] some message sent from the web UI" crap
+xC.hook_irc (function (hook, server, line)
+ local msg, us = xC.parse (line), server.user
+ if not servers[server.name] or msg.command ~= "PRIVMSG" or not us
+ or msg.params[1]:lower () ~= us.nickname:lower () then return line end
+
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local quoted_nick = us.nickname:gsub ("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")
+ local text = line:match ("^.- PRIVMSG .- :%[" .. quoted_nick .. "%] (.*)$")
+ if not text then return line end
+ return ":" .. us.nickname .. "!" .. server.irc_user_host .. " PRIVMSG "
+ .. msg.prefix:match "^[^!@]*" .. " :" .. text
+end)
+
+-- Unfuck emoji and :nick!nick@irc.tinyspeck.com MODE #channel +v nick : active
+xC.hook_irc (function (hook, server, line)
+ if not servers[server.name] then return line end
+ if unemojify then
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ if start then return start .. text:gsub (":([a-z_]+):", function (name)
+ if emoji[name] then return emoji[name] end
+ return ":" .. name .. ":"
+ end) end
+ end
+ return line:gsub ("^(:%S+ MODE .+) : .*", "%1")
+end)
+
+-- The gateway simply ignores the NAMES command altogether
+xC.hook_input (function (hook, buffer, input)
+ if not buffer.channel or not servers[buffer.server.name]
+ or not input:match "^/names%s*$" then return input end
+
+ local users = buffer.channel.users
+ table.sort (users, function (a, b)
+ if a.prefixes > b.prefixes then return true end
+ if a.prefixes < b.prefixes then return false end
+ return a.user.nickname < b.user.nickname
+ end)
+
+ local names = "Users on " .. buffer.channel.name .. ":"
+ for i, chan_user in ipairs (users) do
+ names = names .. " " .. chan_user.prefixes .. chan_user.user.nickname
+ end
+ buffer:log (names)
+end)
+
+xC.hook_completion (function (hook, data, word)
+ local chan = xC.current_buffer.channel
+ local server = xC.current_buffer.server
+ if not chan or not servers[server.name] then return end
+
+ -- In /commands there is typically no desire at all to add the at sign
+ if data.location == 1 and data.words[1]:match "^/" then return end
+
+ -- Handle both when the at sign is already there and when it is not
+ local needle = word:gsub ("^@", ""):lower ()
+
+ local t = {}
+ local try = function (name)
+ if data.location == 0 then name = name .. ":" end
+ if name:sub (1, #needle):lower () == needle then
+ table.insert (t, "@" .. name)
+ end
+ end
+ for _, chan_user in ipairs (chan.users) do
+ try (chan_user.user.nickname)
+ end
+ for _, special in ipairs { "channel", "here" } do
+ try (special)
+ end
+ return t
+end)
diff --git a/plugins/xC/thin-cursor.lua b/plugins/xC/thin-cursor.lua
new file mode 100644
index 0000000..d0fbf38
--- /dev/null
+++ b/plugins/xC/thin-cursor.lua
@@ -0,0 +1,27 @@
+--
+-- thin-cursor.lua: set a thin cursor
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+-- If tmux doesn't work, add the following to its configuration:
+-- set -as terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[2 q'
+-- Change the "2" as per http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+
+local out = io.output ()
+out:write ("\x1b[6 q"):flush ()
+
+-- By registering a global variable, we get notified about plugin unload
+x = setmetatable ({}, { __gc = function ()
+ out:write ("\x1b[2 q"):flush ()
+end })
diff --git a/plugins/xC/utm-filter.lua b/plugins/xC/utm-filter.lua
new file mode 100644
index 0000000..b708c12
--- /dev/null
+++ b/plugins/xC/utm-filter.lua
@@ -0,0 +1,66 @@
+--
+-- utm-filter.lua: filter out Google Analytics bullshit etc. from URLs
+--
+-- Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name>
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- 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.
+--
+
+-- A list of useless URL parameters that don't affect page function
+local banned = {
+ gclid = 1,
+
+ -- Alas, Facebook no longer uses this parameter, see:
+ -- https://news.ycombinator.com/item?id=32117489
+ fbclid = 1,
+
+ utm_source = 1,
+ utm_medium = 1,
+ utm_term = 1,
+ utm_content = 1,
+ utm_campaign = 1,
+}
+
+-- Go through a parameter list and throw out any banned elements
+local do_args = function (args)
+ local filtered = {}
+ for part in args:gmatch ("[^&]+") do
+ if not banned[part:match ("^[^=]*")] then
+ table.insert (filtered, part)
+ end
+ end
+ return table.concat (filtered, "&")
+end
+
+-- Filter parameters in both the query and the fragment part of an URL
+local do_single_url = function (url)
+ return url:gsub ('^([^?#]*)%?([^#]*)', function (start, query)
+ local clean = do_args (query)
+ return #clean > 0 and start .. "?" .. clean or start
+ end, 1):gsub ('^([^#]*)#(.*)', function (start, fragment)
+ local clean = do_args (fragment)
+ return #clean > 0 and start .. "#" .. clean or start
+ end, 1)
+end
+
+local do_text = function (text)
+ return text:gsub ('%f[%g]https?://%g+', do_single_url)
+end
+
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.* :)(.*)$")
+ return message and start .. do_text (message) or line
+end)
+
+xC.hook_input (function (hook, buffer, input)
+ return do_text (input)
+end)
diff --git a/test b/test
new file mode 100755
index 0000000..2ba55b4
--- /dev/null
+++ b/test
@@ -0,0 +1,50 @@
+#!/usr/bin/expect -f
+# Very basic end-to-end testing for CI
+
+# Run the daemon to test against
+system ./xD --write-default-cfg
+spawn ./xD -d
+
+# 10 seconds is a bit too much
+set timeout 5
+
+spawn ./xC
+
+# Fuck this Tcl shit, I want the exit code
+expect_after {
+ eof {
+ puts ""
+ puts "Child exited prematurely"
+ exit 1
+ }
+}
+
+# Connect to the daemon
+send "/server add localhost\n"
+expect "]"
+send "/set servers.localhost.addresses = \"localhost\"\n"
+expect "Option changed"
+send "/disconnect\n"
+expect "]"
+send "/connect\n"
+expect "Connection established"
+
+# Try some chatting
+send "/join #test\n"
+expect "has joined"
+send "Hello\n"
+expect "Hello"
+
+# Attributes
+send "\x1bmbBold text! \x1bmc0,5And colors.\n"
+expect "]"
+
+# Try basic commands
+send "/set\n"
+expect "]"
+send "/help\n"
+expect "]"
+
+# Quit
+send "/quit\n"
+expect "Shutting down"
diff --git a/test-nick-colors b/test-nick-colors
new file mode 100755
index 0000000..b6c97cc
--- /dev/null
+++ b/test-nick-colors
@@ -0,0 +1,26 @@
+#!/bin/sh
+# Check whether the terminal colours filtered by our algorithm are legible
+export example=$(
+ tcc "-run -lm" - <<-END
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <math.h>
+
+ #define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
+
+ $(perl -0777 -ne 'print $& if /^.*?\nfilter_color(?s:.*?)^}$/m' \
+ "$(dirname "$0")"/xC.c)
+
+ void main () {
+ size_t len = 0;
+ int *table = filter_color_cube_for_acceptable_nick_colors (&len);
+ for (size_t i = 0; i < len; i++)
+ printf ("<@\\x1b[38;5;%dmIRCuser\\x1b[m> I'm typing!\n", table[i]);
+ }
+ END
+)
+
+# Both should give acceptable results,
+# which results in a bad compromise that the main author himself needs
+xterm -bg black -fg white -e 'echo $example; cat' &
+xterm -bg white -fg black -e 'echo $example; cat' &
diff --git a/test-static b/test-static
new file mode 100755
index 0000000..85d7f4f
--- /dev/null
+++ b/test-static
@@ -0,0 +1,14 @@
+#!/bin/sh
+# We don't use printf's percent notation with our custom logging mechanism,
+# so the compiler cannot check it for us like it usually does
+perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
+ while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
+ my ($p, $m) = ($`, $&);
+ printf "$ARGV:%d: suspicious log format string: %s...\n",
+ (1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
+ $status = 1;
+ }
+ END {
+ exit $status;
+ }
+END
diff --git a/xB.adoc b/xB.adoc
new file mode 100644
index 0000000..1cf14ab
--- /dev/null
+++ b/xB.adoc
@@ -0,0 +1,104 @@
+xB(1)
+=====
+:doctype: manpage
+:manmanual: xK Manual
+:mansource: xK {release-version}
+
+Name
+----
+xB - modular IRC bot
+
+Synopsis
+--------
+*xB* [_OPTION_]...
+
+Description
+-----------
+*xB* is a modular IRC bot with a programming language-agnostic plugin
+architecture based on co-processes.
+
+Options
+-------
+*-d*, *--debug*::
+ Print more information to help debug various issues.
+
+*-h*, *--help*::
+ Display a help message and exit.
+
+*-V*, *--version*::
+ Output version information and exit.
+
+*--write-default-cfg*[**=**__PATH__]::
+ Write a configuration file with defaults, show its path and exit.
++
+The file will be appropriately commented.
+
+Commands
+--------
+The bot accepts the following commands when they either appear quoted by the
+*prefix* string on a channel or unquoted as a private message sent directly
+to the bot, on the condition that the sending user matches the *admin*
+regular expression or that it is left unset:
+
+*quote* [_message_]::
+ Forwards the message to the IRC server as-is.
+*quit* [_reason_]::
+ Quits the IRC server, with an optional reason string.
+*status*::
+ Sends back a report about its state and all loaded plugins.
+*load* _plugin_[, _plugin_]...::
+ Tries to load the given plugins.
+*unload* _plugin_[, _plugin_]...::
+ Tries to unload the given plugins.
+*reload* _plugin_[, _plugin_]...::
+ The same as *unload* immediately followed by *load*.
+
+Plugins
+-------
+Plugins communicate with the bot over their standard input and output streams
+using the IRC protocol. (Caveat: the standard C library doesn't automatically
+flush FILE streams for pipes on newlines.) A special *XB* command is introduced
+for RPC, with the following subcommands:
+
+*XB get_config* __key__::
+ Request the value of the given configuration option. If no such option
+ exists, the value will be empty. The response will be delivered in
+ the following format:
++
+....
+XB :value
+....
++
+This is particularly useful for retrieving the *prefix* string.
+
+*XB print* _message_::
+ Make the bot print the _message_ on its standard output.
+
+*XB register*::
+ Once a plugin issues this command, it will start receiving all of the bot's
+ incoming IRC traffic, which includes data from the initialization period.
+
+All other commands will be forwarded directly to the IRC server.
+
+Files
+-----
+*xB* follows the XDG Base Directory Specification.
+
+_~/.config/xB/xB.conf_::
+ The bot's configuration file. Use the *--write-default-cfg* option
+ to create a new one for editing.
+
+_~/.local/share/xB/_::
+ The initial working directory for plugins, in which they may create private
+ databases or other files as needed.
+
+_~/.local/share/xB/plugins/_::
+_/usr/local/share/xB/plugins/_::
+_/usr/share/xB/plugins/_::
+ Plugins are searched for in these directories, in order, unless
+ the *plugin_dir* configuration option overrides this.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/xK to report bugs, request features,
+or submit pull requests.
diff --git a/xB.c b/xB.c
new file mode 100644
index 0000000..3713724
--- /dev/null
+++ b/xB.c
@@ -0,0 +1,2064 @@
+/*
+ * xB.c: a modular IRC bot
+ *
+ * Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ */
+
+#include "config.h"
+#define PROGRAM_NAME "xB"
+
+#include "common.c"
+
+// --- Configuration (application-specific) ------------------------------------
+
+static struct simple_config_item g_config_table[] =
+{
+ { "nickname", "xB", "IRC nickname" },
+ { "username", "bot", "IRC user name" },
+ { "realname", "xB IRC bot", "IRC real name/e-mail" },
+
+ { "irc_host", NULL, "Address of the IRC server" },
+ { "irc_port", "6667", "Port of the IRC server" },
+ { "tls", "off", "Whether to use TLS" },
+ { "tls_cert", NULL, "Client TLS certificate (PEM)" },
+ { "tls_verify", "on", "Whether to verify certificates" },
+ { "tls_ca_file", NULL, "OpenSSL CA bundle file" },
+ { "tls_ca_path", NULL, "OpenSSL CA bundle path" },
+ { "autojoin", NULL, "Channels to join on start" },
+ { "reconnect", "on", "Whether to reconnect on error" },
+ { "reconnect_delay", "5", "Time between reconnecting" },
+
+ { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" },
+ { "socks_port", "1080", "SOCKS port number" },
+ { "socks_username", NULL, "SOCKS auth. username" },
+ { "socks_password", NULL, "SOCKS auth. password" },
+
+ { "prefix", ":", "The prefix for bot commands" },
+ { "admin", NULL, "Host mask for administrators" },
+ { "plugins", NULL, "The plugins to load on startup" },
+ { "plugin_dir", NULL, "Plugin search path override" },
+ { "recover", "on", "Whether to re-launch on crash" },
+
+ { NULL, NULL, NULL }
+};
+
+// --- Application data --------------------------------------------------------
+
+struct plugin
+{
+ LIST_HEADER (struct plugin)
+ struct bot_context *ctx; ///< Parent context
+
+ char *name; ///< Plugin identifier
+ pid_t pid; ///< PID of the plugin process
+
+ bool is_zombie; ///< Whether the child is a zombie
+ bool initialized; ///< Ready to exchange IRC messages
+ struct str queued_output; ///< Output queued up until initialized
+
+ // Since we're doing non-blocking I/O, we need to queue up data so that
+ // we don't stall on plugins unnecessarily.
+
+ int read_fd; ///< The read end of the comm. pipe
+ int write_fd; ///< The write end of the comm. pipe
+
+ struct poller_fd read_event; ///< Read FD event
+ struct poller_fd write_event; ///< Write FD event
+
+ struct str read_buffer; ///< Unprocessed input
+ struct str write_buffer; ///< Output yet to be sent out
+};
+
+static struct plugin *
+plugin_new (void)
+{
+ struct plugin *self = xcalloc (1, sizeof *self);
+ self->pid = -1;
+ self->queued_output = str_make ();
+
+ self->read_fd = -1;
+ self->read_buffer = str_make ();
+ self->write_fd = -1;
+ self->write_buffer = str_make ();
+ return self;
+}
+
+static void
+plugin_destroy (struct plugin *self)
+{
+ soft_assert (self->pid == -1);
+ free (self->name);
+
+ str_free (&self->read_buffer);
+ if (!soft_assert (self->read_fd == -1))
+ xclose (self->read_fd);
+
+ str_free (&self->write_buffer);
+ if (!soft_assert (self->write_fd == -1))
+ xclose (self->write_fd);
+
+ if (!self->initialized)
+ str_free (&self->queued_output);
+
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct bot_context
+{
+ struct str_map config; ///< User configuration
+ regex_t *admin_re; ///< Regex to match our administrator
+ bool reconnect; ///< Whether to reconnect on conn. fail.
+ unsigned long reconnect_delay; ///< Reconnect delay in seconds
+
+ int irc_fd; ///< Socket FD of the server
+ struct str read_buffer; ///< Input yet to be processed
+ struct poller_fd irc_event; ///< IRC FD event
+ bool irc_registered; ///< Whether we may send messages now
+
+ struct poller_fd signal_event; ///< Signal FD event
+ struct poller_timer ping_tmr; ///< We should send a ping
+ struct poller_timer timeout_tmr; ///< Connection seems to be dead
+ struct poller_timer reconnect_tmr; ///< We should reconnect now
+
+ SSL_CTX *ssl_ctx; ///< SSL context
+ SSL *ssl; ///< SSL connection
+
+ struct plugin *plugins; ///< Linked list of plugins
+ struct str_map plugins_by_name; ///< Indexes @em plugins by their name
+
+ struct poller poller; ///< Manages polled descriptors
+ bool quitting; ///< User requested quitting
+ bool polling; ///< The event loop is running
+};
+
+static void on_irc_ping_timeout (void *user_data);
+static void on_irc_timeout (void *user_data);
+static void on_irc_reconnect_timeout (void *user_data);
+
+static void
+bot_context_init (struct bot_context *self)
+{
+ self->config = str_map_make (free);
+ simple_config_load_defaults (&self->config, g_config_table);
+ self->admin_re = NULL;
+
+ self->irc_fd = -1;
+ self->read_buffer = str_make ();
+ self->irc_registered = false;
+
+ self->ssl = NULL;
+ self->ssl_ctx = NULL;
+
+ self->plugins = NULL;
+ self->plugins_by_name = str_map_make (NULL);
+
+ poller_init (&self->poller);
+ self->quitting = false;
+ self->polling = false;
+
+ self->timeout_tmr = poller_timer_make (&self->poller);
+ self->timeout_tmr.dispatcher = on_irc_timeout;
+ self->timeout_tmr.user_data = self;
+
+ self->ping_tmr = poller_timer_make (&self->poller);
+ self->ping_tmr.dispatcher = on_irc_ping_timeout;
+ self->ping_tmr.user_data = self;
+
+ self->reconnect_tmr = poller_timer_make (&self->poller);
+ self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout;
+ self->reconnect_tmr.user_data = self;
+}
+
+static void
+bot_context_free (struct bot_context *self)
+{
+ str_map_free (&self->config);
+ if (self->admin_re)
+ regex_free (self->admin_re);
+ str_free (&self->read_buffer);
+
+ // TODO: terminate the plugins properly before this is called
+ LIST_FOR_EACH (struct plugin, link, self->plugins)
+ plugin_destroy (link);
+
+ if (self->irc_fd != -1)
+ {
+ poller_fd_reset (&self->irc_event);
+ xclose (self->irc_fd);
+ }
+ if (self->ssl)
+ SSL_free (self->ssl);
+ if (self->ssl_ctx)
+ SSL_CTX_free (self->ssl_ctx);
+
+ str_map_free (&self->plugins_by_name);
+ poller_free (&self->poller);
+}
+
+static void
+irc_shutdown (struct bot_context *ctx)
+{
+ // TODO: set a timer after which we cut the connection?
+ // Generally non-critical
+ if (ctx->ssl)
+ soft_assert (SSL_shutdown (ctx->ssl) != -1);
+ else
+ soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0);
+}
+
+static void
+try_finish_quit (struct bot_context *ctx)
+{
+ if (ctx->quitting && ctx->irc_fd == -1 && !ctx->plugins)
+ ctx->polling = false;
+}
+
+static bool plugin_zombify (struct plugin *);
+
+static void
+initiate_quit (struct bot_context *ctx)
+{
+ // Initiate bringing down of the two things that block our shutdown:
+ // a/ the IRC socket, b/ our child processes:
+
+ for (struct plugin *plugin = ctx->plugins;
+ plugin; plugin = plugin->next)
+ plugin_zombify (plugin);
+ if (ctx->irc_fd != -1)
+ irc_shutdown (ctx);
+
+ ctx->quitting = true;
+ try_finish_quit (ctx);
+}
+
+static bool irc_send (struct bot_context *ctx,
+ const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
+
+static bool
+irc_send (struct bot_context *ctx, const char *format, ...)
+{
+ va_list ap;
+
+ if (g_debug_mode)
+ {
+ fputs ("[IRC] <== \"", stderr);
+ va_start (ap, format);
+ vfprintf (stderr, format, ap);
+ va_end (ap);
+ fputs ("\"\n", stderr);
+ }
+
+ if (!soft_assert (ctx->irc_fd != -1))
+ return false;
+
+ va_start (ap, format);
+ struct str str = str_make ();
+ str_append_vprintf (&str, format, ap);
+ str_append (&str, "\r\n");
+ va_end (ap);
+
+ bool result = true;
+ if (ctx->ssl)
+ {
+ // TODO: call SSL_get_error() to detect if a clean shutdown has occured
+ ERR_clear_error ();
+ if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len)
+ {
+ print_debug ("%s: %s: %s", __func__, "SSL_write",
+ xerr_describe_error ());
+ result = false;
+ }
+ }
+ else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len)
+ {
+ print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
+ result = false;
+ }
+
+ str_free (&str);
+ return result;
+}
+
+static bool
+irc_get_boolean_from_config
+ (struct bot_context *ctx, const char *name, bool *value, struct error **e)
+{
+ const char *str = str_map_find (&ctx->config, name);
+ hard_assert (str != NULL);
+
+ if (set_boolean_if_valid (value, str))
+ return true;
+
+ return error_set (e, "invalid configuration value for `%s'", name);
+}
+
+static bool
+irc_initialize_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
+ struct error **e)
+{
+ ERR_clear_error ();
+
+ if (file || path)
+ {
+ if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
+ return true;
+
+ return error_set (e, "%s: %s",
+ "failed to set locations for the CA certificate bundle",
+ xerr_describe_error ());
+ }
+
+ if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
+ return error_set (e, "%s: %s",
+ "couldn't load the default CA certificate bundle",
+ xerr_describe_error ());
+ return true;
+}
+
+static bool
+irc_initialize_ca (struct bot_context *ctx, struct error **e)
+{
+ const char *ca_file = str_map_find (&ctx->config, "tls_ca_file");
+ const char *ca_path = str_map_find (&ctx->config, "tls_ca_path");
+
+ char *full_file = ca_file
+ ? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
+ char *full_path = ca_path
+ ? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
+
+ bool ok = false;
+ if (ca_file && !full_file)
+ error_set (e, "couldn't find the CA bundle file");
+ else if (ca_path && !full_path)
+ error_set (e, "couldn't find the CA bundle path");
+ else
+ ok = irc_initialize_ca_set (ctx->ssl_ctx, full_file, full_path, e);
+
+ free (full_file);
+ free (full_path);
+ return ok;
+}
+
+static bool
+irc_initialize_ssl_ctx (struct bot_context *ctx, struct error **e)
+{
+ // Disable deprecated protocols (see RFC 7568)
+ SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ bool verify;
+ if (!irc_get_boolean_from_config (ctx, "tls_verify", &verify, e))
+ return false;
+ SSL_CTX_set_verify (ctx->ssl_ctx,
+ verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
+
+ struct error *error = NULL;
+ if (!irc_initialize_ca (ctx, &error))
+ {
+ if (verify)
+ {
+ error_propagate (e, error);
+ return false;
+ }
+
+ // Only inform the user if we're not actually verifying
+ print_warning ("%s", error->message);
+ error_free (error);
+ }
+ return true;
+}
+
+static bool
+irc_initialize_tls (struct bot_context *ctx, struct error **e)
+{
+ const char *error_info = NULL;
+ ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
+ if (!ctx->ssl_ctx)
+ goto error_ssl_1;
+ if (!irc_initialize_ssl_ctx (ctx, e))
+ goto error_ssl_2;
+
+ ctx->ssl = SSL_new (ctx->ssl_ctx);
+ if (!ctx->ssl)
+ goto error_ssl_2;
+
+ const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
+ if (tls_cert)
+ {
+ char *path = resolve_filename
+ (tls_cert, resolve_relative_config_filename);
+ if (!path)
+ print_error ("%s: %s", "cannot open file", tls_cert);
+ // XXX: perhaps we should read the file ourselves for better messages
+ else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)
+ || !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM))
+ print_error ("%s: %s", "setting the TLS client certificate failed",
+ xerr_describe_error ());
+ free (path);
+ }
+
+ SSL_set_connect_state (ctx->ssl);
+ if (!SSL_set_fd (ctx->ssl, ctx->irc_fd))
+ goto error_ssl_3;
+ // Avoid SSL_write() returning SSL_ERROR_WANT_READ
+ SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY);
+
+ switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info))
+ {
+ case SSL_ERROR_NONE:
+ return true;
+ case SSL_ERROR_ZERO_RETURN:
+ error_info = "server closed the connection";
+ default:
+ break;
+ }
+
+error_ssl_3:
+ SSL_free (ctx->ssl);
+ ctx->ssl = NULL;
+error_ssl_2:
+ SSL_CTX_free (ctx->ssl_ctx);
+ ctx->ssl_ctx = NULL;
+error_ssl_1:
+ if (!error_info)
+ error_info = xerr_describe_error ();
+ return error_set (e, "%s: %s", "could not initialize TLS", error_info);
+}
+
+static bool
+irc_establish_connection (struct bot_context *ctx,
+ const char *host, const char *port, struct error **e)
+{
+ struct addrinfo gai_hints, *gai_result, *gai_iter;
+ memset (&gai_hints, 0, sizeof gai_hints);
+ gai_hints.ai_socktype = SOCK_STREAM;
+
+ int err = getaddrinfo (host, port, &gai_hints, &gai_result);
+ if (err)
+ return error_set (e, "%s: %s: %s", "connection failed",
+ "getaddrinfo", gai_strerror (err));
+
+ int sockfd;
+ for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
+ {
+ sockfd = socket (gai_iter->ai_family,
+ gai_iter->ai_socktype, gai_iter->ai_protocol);
+ if (sockfd == -1)
+ continue;
+ set_cloexec (sockfd);
+
+ int yes = 1;
+ soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
+ &yes, sizeof yes) != -1);
+
+ const char *real_host = host;
+
+ // Let's try to resolve the address back into a real hostname;
+ // we don't really need this, so we can let it quietly fail
+ char buf[NI_MAXHOST];
+ err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
+ buf, sizeof buf, NULL, 0, 0);
+ if (err)
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
+ else
+ real_host = buf;
+
+ // XXX: we shouldn't mix these statuses with `struct error'; choose 1!
+ char *address = format_host_port_pair (real_host, port);
+ print_status ("connecting to %s...", address);
+ free (address);
+
+ if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
+ break;
+
+ xclose (sockfd);
+ }
+
+ freeaddrinfo (gai_result);
+
+ if (!gai_iter)
+ return error_set (e, "connection failed");
+
+ ctx->irc_fd = sockfd;
+ return true;
+}
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+static struct strv
+ g_original_argv, ///< Original program arguments
+ g_recovery_env; ///< Environment for re-exec recovery
+
+/// Program termination has been requested by a signal
+static volatile sig_atomic_t g_termination_requested;
+
+/// Points to startup reason location within `g_recovery_environment'
+static char **g_startup_reason_location;
+/// The environment variable used to pass the startup reason when re-executing
+static const char g_startup_reason_str[] = "STARTUP_REASON";
+
+static void
+sigchld_handler (int signum)
+{
+ (void) signum;
+
+ int original_errno = errno;
+ // Just so that the read end of the pipe wakes up the poller.
+ // NOTE: Linux has signalfd() and eventfd(), and the BSD's have kqueue.
+ // All of them are better than this approach, although platform-specific.
+ if (write (g_signal_pipe[1], "c", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+sigterm_handler (int signum)
+{
+ (void) signum;
+
+ g_termination_requested = true;
+
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], "t", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+setup_signal_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = sigchld_handler;
+ sigemptyset (&sa.sa_mask);
+
+ if (sigaction (SIGCHLD, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+
+ signal (SIGPIPE, SIG_IGN);
+
+ sa.sa_handler = sigterm_handler;
+ if (sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+}
+
+static void
+translate_signal_info (int no, const char **name, int code, const char **reason)
+{
+ if (code == SI_USER) *reason = "signal sent by kill()";
+ if (code == SI_QUEUE) *reason = "signal sent by sigqueue()";
+
+ switch (no)
+ {
+ case SIGILL:
+ *name = "SIGILL";
+ if (code == ILL_ILLOPC) *reason = "illegal opcode";
+ if (code == ILL_ILLOPN) *reason = "illegal operand";
+ if (code == ILL_ILLADR) *reason = "illegal addressing mode";
+ if (code == ILL_ILLTRP) *reason = "illegal trap";
+ if (code == ILL_PRVOPC) *reason = "privileged opcode";
+ if (code == ILL_PRVREG) *reason = "privileged register";
+ if (code == ILL_COPROC) *reason = "coprocessor error";
+ if (code == ILL_BADSTK) *reason = "internal stack error";
+ break;
+ case SIGFPE:
+ *name = "SIGFPE";
+ if (code == FPE_INTDIV) *reason = "integer divide by zero";
+ if (code == FPE_INTOVF) *reason = "integer overflow";
+ if (code == FPE_FLTDIV) *reason = "floating-point divide by zero";
+ if (code == FPE_FLTOVF) *reason = "floating-point overflow";
+ if (code == FPE_FLTUND) *reason = "floating-point underflow";
+ if (code == FPE_FLTRES) *reason = "floating-point inexact result";
+ if (code == FPE_FLTINV) *reason = "invalid floating-point operation";
+ if (code == FPE_FLTSUB) *reason = "subscript out of range";
+ break;
+ case SIGSEGV:
+ *name = "SIGSEGV";
+ if (code == SEGV_MAPERR)
+ *reason = "address not mapped to object";
+ if (code == SEGV_ACCERR)
+ *reason = "invalid permissions for mapped object";
+ break;
+ case SIGBUS:
+ *name = "SIGBUS";
+ if (code == BUS_ADRALN) *reason = "invalid address alignment";
+ if (code == BUS_ADRERR) *reason = "nonexistent physical address";
+ if (code == BUS_OBJERR) *reason = "object-specific hardware error";
+ break;
+ default:
+ *name = NULL;
+ }
+}
+
+static void
+recovery_handler (int signum, siginfo_t *info, void *context)
+{
+ (void) context;
+
+ // TODO: maybe try to force a core dump like this: if (fork() == 0) return;
+ // TODO: maybe we could even send "\r\nQUIT :reason\r\n" to the server. >_>
+ // As long as we're not connected via TLS, that is.
+
+ const char *signal_name = NULL, *reason = NULL;
+ translate_signal_info (signum, &signal_name, info->si_code, &reason);
+
+ char buf[128], numbuf[8];
+ if (!signal_name)
+ {
+ snprintf (numbuf, sizeof numbuf, "%d", signum);
+ signal_name = numbuf;
+ }
+
+ if (reason)
+ snprintf (buf, sizeof buf, "%s=%s: %s: %s", g_startup_reason_str,
+ "signal received", signal_name, reason);
+ else
+ snprintf (buf, sizeof buf, "%s=%s: %s", g_startup_reason_str,
+ "signal received", signal_name);
+ *g_startup_reason_location = buf;
+
+ // Avoid annoying resource intensive infinite loops by sleeping for a bit
+ (void) sleep (1);
+
+ // TODO: maybe pregenerate the path, see the following for some other ways
+ // that would be illegal to do from within a signal handler:
+ // http://stackoverflow.com/a/1024937
+ // http://stackoverflow.com/q/799679
+ // Especially if we change the current working directory in the program.
+ //
+ // Note that I can just overwrite g_orig_argv[0].
+
+ // NOTE: our children will read EOF on the read ends of their pipes as a
+ // a result of O_CLOEXEC. That should be enough to make them terminate.
+
+ char **argv = g_original_argv.vector, **argp = g_recovery_env.vector;
+ execve ("/proc/self/exe", argv, argp); // Linux
+ execve ("/proc/curproc/file", argv, argp); // BSD
+ execve ("/proc/curproc/exe", argv, argp); // BSD
+ execve ("/proc/self/path/a.out", argv, argp); // Solaris
+ execve (argv[0], argv, argp); // unreliable fallback
+
+ // Let's just crash
+ perror ("execve");
+ signal (signum, SIG_DFL);
+ raise (signum);
+}
+
+static void
+prepare_recovery_environment (void)
+{
+ g_recovery_env = strv_make ();
+ strv_append_vector (&g_recovery_env, environ);
+
+ // Prepare a location within the environment where we will put the startup
+ // (or maybe rather restart) reason in case of an irrecoverable error.
+ char **iter;
+ for (iter = g_recovery_env.vector; *iter; iter++)
+ {
+ const size_t len = sizeof g_startup_reason_str - 1;
+ if (!strncmp (*iter, g_startup_reason_str, len) && (*iter)[len] == '=')
+ break;
+ }
+
+ if (*iter)
+ g_startup_reason_location = iter;
+ else
+ {
+ g_startup_reason_location = g_recovery_env.vector + g_recovery_env.len;
+ strv_append (&g_recovery_env, "");
+ }
+}
+
+static bool
+setup_recovery_handler (struct bot_context *ctx, struct error **e)
+{
+ bool recover;
+ if (!irc_get_boolean_from_config (ctx, "recover", &recover, e))
+ return false;
+ if (!recover)
+ return true;
+
+ // Make sure these signals aren't blocked, otherwise we would be unable
+ // to handle them, making the critical conditions fatal.
+ sigset_t mask;
+ sigemptyset (&mask);
+ sigaddset (&mask, SIGSEGV);
+ sigaddset (&mask, SIGBUS);
+ sigaddset (&mask, SIGFPE);
+ sigaddset (&mask, SIGILL);
+ sigprocmask (SIG_UNBLOCK, &mask, NULL);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_SIGINFO;
+ sa.sa_sigaction = recovery_handler;
+ sigemptyset (&sa.sa_mask);
+
+ prepare_recovery_environment ();
+
+ // TODO: also handle SIGABRT... or avoid doing abort() in the first place?
+ if (sigaction (SIGSEGV, &sa, NULL) == -1
+ || sigaction (SIGBUS, &sa, NULL) == -1
+ || sigaction (SIGFPE, &sa, NULL) == -1
+ || sigaction (SIGILL, &sa, NULL) == -1)
+ print_error ("sigaction: %s", strerror (errno));
+ return true;
+}
+
+// --- Plugins -----------------------------------------------------------------
+
+/// The name of the special IRC command for interprocess communication
+static const char *plugin_ipc_command = "XB";
+
+static struct plugin *
+plugin_find_by_pid (struct bot_context *ctx, pid_t pid)
+{
+ struct plugin *iter;
+ for (iter = ctx->plugins; iter; iter = iter->next)
+ if (iter->pid == pid)
+ return iter;
+ return NULL;
+}
+
+static bool
+plugin_zombify (struct plugin *plugin)
+{
+ if (plugin->is_zombie)
+ return false;
+
+ // FIXME: make sure that we don't remove entries from the poller while we
+ // still may have stuff to read; maybe just check that the read pipe is
+ // empty before closing it... and then on EOF check if `pid == -1' and
+ // only then dispose of it (it'd be best to simulate that both of these
+ // cases may happen).
+ poller_fd_reset (&plugin->write_event);
+
+ // TODO: try to flush the write buffer (non-blocking)?
+
+ // The plugin should terminate itself after it receives EOF.
+ xclose (plugin->write_fd);
+ plugin->write_fd = -1;
+
+ // Make it a pseudo-anonymous zombie. In this state we process any
+ // remaining commands it attempts to send to us before it finally dies.
+ str_map_set (&plugin->ctx->plugins_by_name, plugin->name, NULL);
+ plugin->is_zombie = true;
+
+ // TODO: wait a few seconds and then send SIGKILL to the plugin
+ return true;
+}
+
+static void
+on_plugin_writable (const struct pollfd *fd, struct plugin *plugin)
+{
+ struct str *buf = &plugin->write_buffer;
+ size_t written_total = 0;
+
+ if (fd->revents & ~(POLLOUT | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ while (written_total != buf->len)
+ {
+ ssize_t n_written = write (fd->fd, buf->str + written_total,
+ buf->len - written_total);
+
+ if (n_written < 0)
+ {
+ if (errno == EAGAIN)
+ break;
+ if (errno == EINTR)
+ continue;
+
+ soft_assert (errno == EPIPE);
+ // Zombies shouldn't get dispatched for writability
+ hard_assert (!plugin->is_zombie);
+
+ print_debug ("%s: %s", "write", strerror (errno));
+ print_error ("failure on writing to plugin `%s',"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ break;
+ }
+
+ // This may be equivalent to EAGAIN on some implementations
+ if (n_written == 0)
+ break;
+
+ written_total += n_written;
+ }
+
+ if (written_total != 0)
+ str_remove_slice (buf, 0, written_total);
+
+ if (buf->len == 0)
+ // Everything has been written, there's no need to end up in here again
+ poller_fd_reset (&plugin->write_event);
+}
+
+static void
+plugin_queue_write (struct plugin *plugin)
+{
+ if (plugin->is_zombie)
+ return;
+
+ // Don't let the write buffer grow indefinitely. If there's a ton of data
+ // waiting to be processed by the plugin, it usually means there's something
+ // wrong with it (such as someone stopping the process).
+ if (plugin->write_buffer.len >= (1 << 20))
+ {
+ print_warning ("plugin `%s' does not seem to process messages fast"
+ " enough, I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ return;
+ }
+ poller_fd_set (&plugin->write_event, POLLOUT);
+}
+
+static void
+plugin_send (struct plugin *plugin, const char *format, ...)
+ ATTRIBUTE_PRINTF (2, 3);
+
+static void
+plugin_send (struct plugin *plugin, const char *format, ...)
+{
+ va_list ap;
+
+ if (g_debug_mode)
+ {
+ fprintf (stderr, "[%s] <-- \"", plugin->name);
+ va_start (ap, format);
+ vfprintf (stderr, format, ap);
+ va_end (ap);
+ fputs ("\"\n", stderr);
+ }
+
+ va_start (ap, format);
+ str_append_vprintf (&plugin->write_buffer, format, ap);
+ va_end (ap);
+ str_append (&plugin->write_buffer, "\r\n");
+
+ plugin_queue_write (plugin);
+}
+
+static void
+plugin_process_ipc (struct plugin *plugin, const struct irc_message *msg)
+{
+ // Replies are sent in the order in which they came in, so there's
+ // no need to attach a special identifier to them. It might be
+ // desirable in some cases, though.
+
+ if (msg->params.len < 1)
+ return;
+
+ const char *command = msg->params.vector[0];
+ if (!plugin->initialized && !strcasecmp (command, "register"))
+ {
+ // Register for relaying of IRC traffic
+ plugin->initialized = true;
+
+ // Flush any queued up traffic here. The point of queuing it in
+ // the first place is so that we don't have to wait for plugin
+ // initialization during startup.
+ //
+ // Note that if we start filtering data coming to the plugins e.g.
+ // based on what it tells us upon registration, we might need to
+ // filter `queued_output' as well.
+ str_append_str (&plugin->write_buffer, &plugin->queued_output);
+ str_free (&plugin->queued_output);
+
+ // NOTE: this may trigger the buffer length check
+ plugin_queue_write (plugin);
+ }
+ else if (!strcasecmp (command, "get_config"))
+ {
+ if (msg->params.len < 2)
+ return;
+
+ const char *value =
+ str_map_find (&plugin->ctx->config, msg->params.vector[1]);
+ // TODO: escape the value (although there's no need to ATM)
+ plugin_send (plugin, "%s :%s",
+ plugin_ipc_command, value ? value : "");
+ }
+ else if (!strcasecmp (command, "print"))
+ {
+ if (msg->params.len < 2)
+ return;
+
+ printf ("%s\n", msg->params.vector[1]);
+ }
+}
+
+static void
+plugin_process_message (const struct irc_message *msg,
+ const char *raw, void *user_data)
+{
+ struct plugin *plugin = user_data;
+ struct bot_context *ctx = plugin->ctx;
+
+ if (g_debug_mode)
+ fprintf (stderr, "[%s] --> \"%s\"\n", plugin->name, raw);
+
+ if (!strcasecmp (msg->command, plugin_ipc_command))
+ plugin_process_ipc (plugin, msg);
+ else if (plugin->initialized && ctx->irc_registered)
+ {
+ // Pass everything else through to the IRC server
+ // XXX: when the server isn't ready yet, these messages get silently
+ // discarded, which shouldn't pose a problem most of the time.
+ // Perhaps we could send a "connected" notification on `register'
+ // if `irc_ready' is true, or after it becomes true later, so that
+ // plugins know when to start sending unprovoked IRC messages.
+ // XXX: another case is when the connection gets interrupted and the
+ // plugin tries to send something back while we're reconnecting.
+ // For that we might set up a global buffer that gets flushed out
+ // after `irc_ready' becomes true. Note that there is always some
+ // chance of messages getting lost without us even noticing it.
+ irc_send (ctx, "%s", raw);
+ }
+}
+
+static void
+on_plugin_readable (const struct pollfd *fd, struct plugin *plugin)
+{
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ // TODO: see if I can reuse irc_fill_read_buffer()
+ struct str *buf = &plugin->read_buffer;
+ while (true)
+ {
+ str_reserve (buf, 512 + 1);
+ ssize_t n_read = read (fd->fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1);
+
+ if (n_read < 0)
+ {
+ if (errno == EAGAIN)
+ break;
+ if (soft_assert (errno == EINTR))
+ continue;
+
+ if (!plugin->is_zombie)
+ {
+ print_error ("failure on reading from plugin `%s',"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ }
+ return;
+ }
+
+ // EOF; hopefully it will die soon (maybe it already has)
+ if (n_read == 0)
+ break;
+
+ buf->str[buf->len += n_read] = '\0';
+ if (buf->len >= (1 << 20))
+ {
+ // XXX: this isn't really the best flood prevention mechanism,
+ // but it wasn't even supposed to be one.
+ if (plugin->is_zombie)
+ {
+ print_error ("a zombie of plugin `%s' is trying to flood us,"
+ " therefore I'm killing it", plugin->name);
+ kill (plugin->pid, SIGKILL);
+ }
+ else
+ {
+ print_error ("plugin `%s' seems to spew out data frantically,"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ }
+ return;
+ }
+ }
+
+ irc_process_buffer (buf, plugin_process_message, plugin);
+}
+
+static bool
+is_valid_plugin_name (const char *name)
+{
+ if (!*name)
+ return false;
+ for (const char *p = name; *p; p++)
+ if (!isgraph (*p) || *p == '/')
+ return false;
+ return true;
+}
+
+static char *
+plugin_resolve_relative_filename (const char *filename)
+{
+ struct strv paths = strv_make ();
+ get_xdg_data_dirs (&paths);
+ strv_append (&paths, PROJECT_DATADIR);
+ char *result = resolve_relative_filename_generic
+ (&paths, PROGRAM_NAME "/plugins/", filename);
+ strv_free (&paths);
+ return result;
+}
+
+static struct plugin *
+plugin_launch (struct bot_context *ctx, const char *name, struct error **e)
+{
+ char *path = NULL;
+ const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir");
+ if (plugin_dir)
+ {
+ // resolve_relative_filename_generic() won't accept relative paths,
+ // so just keep the old behaviour and expect the file to exist.
+ // We could use resolve_filename() on "plugin_dir" with paths=getcwd().
+ path = xstrdup_printf ("%s/%s", plugin_dir, name);
+ }
+ else if (!(path = plugin_resolve_relative_filename (name)))
+ {
+ error_set (e, "plugin not found");
+ goto fail_0;
+ }
+
+ int stdin_pipe[2];
+ if (pipe (stdin_pipe) == -1)
+ {
+ error_set (e, "%s: %s", "pipe", strerror (errno));
+ goto fail_0;
+ }
+
+ int stdout_pipe[2];
+ if (pipe (stdout_pipe) == -1)
+ {
+ error_set (e, "%s: %s", "pipe", strerror (errno));
+ goto fail_1;
+ }
+
+ struct str work_dir = str_make ();
+ get_xdg_home_dir (&work_dir, "XDG_DATA_HOME", ".local/share");
+ str_append_printf (&work_dir, "/%s", PROGRAM_NAME);
+
+ if (!mkdir_with_parents (work_dir.str, e))
+ goto fail_2;
+
+ set_cloexec (stdin_pipe[1]);
+ set_cloexec (stdout_pipe[0]);
+
+ pid_t pid = fork ();
+ if (pid == -1)
+ {
+ error_set (e, "%s: %s", "fork", strerror (errno));
+ goto fail_2;
+ }
+
+ if (pid == 0)
+ {
+ // Redirect the child's stdin and stdout to the pipes
+ if (dup2 (stdin_pipe[0], STDIN_FILENO) == -1
+ || dup2 (stdout_pipe[1], STDOUT_FILENO) == -1)
+ {
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "dup2", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+ if (chdir (work_dir.str))
+ {
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "chdir", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+
+ xclose (stdin_pipe[0]);
+ xclose (stdout_pipe[1]);
+
+ // Restore some of the signal handling
+ signal (SIGPIPE, SIG_DFL);
+
+ char *argv[] = { path, NULL };
+ execve (argv[0], argv, environ);
+
+ // We will collect the failure later via SIGCHLD
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "exec", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+
+ str_free (&work_dir);
+ free (path);
+
+ xclose (stdin_pipe[0]);
+ xclose (stdout_pipe[1]);
+
+ struct plugin *plugin = plugin_new ();
+ plugin->ctx = ctx;
+ plugin->pid = pid;
+ plugin->name = xstrdup (name);
+ plugin->read_fd = stdout_pipe[0];
+ plugin->write_fd = stdin_pipe[1];
+ return plugin;
+
+fail_2:
+ str_free (&work_dir);
+ xclose (stdout_pipe[0]);
+ xclose (stdout_pipe[1]);
+fail_1:
+ xclose (stdin_pipe[0]);
+ xclose (stdin_pipe[1]);
+fail_0:
+ free (path);
+ return NULL;
+}
+
+static bool
+plugin_load (struct bot_context *ctx, const char *name, struct error **e)
+{
+ if (!is_valid_plugin_name (name))
+ return error_set (e, "invalid plugin name");
+ if (str_map_find (&ctx->plugins_by_name, name))
+ return error_set (e, "the plugin has already been loaded");
+
+ struct plugin *plugin;
+ if (!(plugin = plugin_launch (ctx, name, e)))
+ return false;
+
+ set_blocking (plugin->read_fd, false);
+ set_blocking (plugin->write_fd, false);
+
+ plugin->read_event = poller_fd_make (&ctx->poller, plugin->read_fd);
+ plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable;
+ plugin->read_event.user_data = plugin;
+
+ plugin->write_event = poller_fd_make (&ctx->poller, plugin->write_fd);
+ plugin->write_event.dispatcher = (poller_fd_fn) on_plugin_writable;
+ plugin->write_event.user_data = plugin;
+
+ LIST_PREPEND (ctx->plugins, plugin);
+ str_map_set (&ctx->plugins_by_name, name, plugin);
+
+ poller_fd_set (&plugin->read_event, POLLIN);
+ return true;
+}
+
+static bool
+plugin_unload (struct bot_context *ctx, const char *name, struct error **e)
+{
+ struct plugin *plugin = str_map_find (&ctx->plugins_by_name, name);
+
+ if (!plugin)
+ return error_set (e, "no such plugin is loaded");
+
+ plugin_zombify (plugin);
+
+ // TODO: add a `kill zombies' command to forcefully get rid of processes
+ // that do not understand the request.
+ return true;
+}
+
+static void
+plugin_load_all_from_config (struct bot_context *ctx)
+{
+ const char *plugin_list = str_map_find (&ctx->config, "plugins");
+ if (!plugin_list)
+ return;
+
+ struct strv plugins = strv_make ();
+ cstr_split (plugin_list, ",", true, &plugins);
+ for (size_t i = 0; i < plugins.len; i++)
+ {
+ char *name = cstr_strip_in_place (plugins.vector[i], " ");
+
+ struct error *e = NULL;
+ if (!plugin_load (ctx, name, &e))
+ {
+ print_error ("plugin `%s' failed to load: %s", name, e->message);
+ error_free (e);
+ }
+ }
+
+ strv_free (&plugins);
+}
+
+// --- Main program ------------------------------------------------------------
+
+static bool
+parse_bot_command (const char *s, const char *command, const char **following)
+{
+ size_t command_len = strlen (command);
+ if (strncasecmp (s, command, command_len))
+ return false;
+ s += command_len;
+
+ // Expect a word boundary, so that we don't respond to invalid things
+ if (isalnum (*s))
+ return false;
+
+ // Ignore any initial spaces; the rest is the command's argument
+ while (isblank (*s))
+ s++;
+ *following = s;
+ return true;
+}
+
+static void
+split_bot_command_argument_list (const char *arguments, struct strv *out)
+{
+ cstr_split (arguments, ",", true, out);
+ for (size_t i = 0; i < out->len; )
+ {
+ if (!*cstr_strip_in_place (out->vector[i], " \t"))
+ strv_remove (out, i);
+ else
+ i++;
+ }
+}
+
+static bool
+is_private_message (const struct irc_message *msg)
+{
+ hard_assert (msg->params.len);
+ return !strchr ("#&+!", *msg->params.vector[0]);
+}
+
+static bool
+is_sent_by_admin (struct bot_context *ctx, const struct irc_message *msg)
+{
+ // No administrator set -> everyone is an administrator
+ if (!ctx->admin_re)
+ return true;
+ return regexec (ctx->admin_re, msg->prefix, 0, NULL, 0) != REG_NOMATCH;
+}
+
+static void respond_to_user (struct bot_context *ctx, const struct
+ irc_message *msg, const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
+
+static void
+respond_to_user (struct bot_context *ctx, const struct irc_message *msg,
+ const char *format, ...)
+{
+ if (!soft_assert (msg->prefix && msg->params.len))
+ return;
+
+ char nick[strcspn (msg->prefix, "!") + 1];
+ strncpy (nick, msg->prefix, sizeof nick - 1);
+ nick[sizeof nick - 1] = '\0';
+
+ va_list ap;
+ struct str text = str_make ();
+ va_start (ap, format);
+ str_append_vprintf (&text, format, ap);
+ va_end (ap);
+
+ if (is_private_message (msg))
+ irc_send (ctx, "PRIVMSG %s :%s", nick, text.str);
+ else
+ irc_send (ctx, "PRIVMSG %s :%s: %s",
+ msg->params.vector[0], nick, text.str);
+
+ str_free (&text);
+}
+
+static void
+process_plugin_load (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ struct error *e = NULL;
+ if (plugin_load (ctx, name, &e))
+ respond_to_user (ctx, msg, "plugin `%s' queued for loading", name);
+ else
+ {
+ respond_to_user (ctx, msg, "plugin `%s' could not be loaded: %s",
+ name, e->message);
+ error_free (e);
+ }
+}
+
+static void
+process_plugin_unload (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ struct error *e = NULL;
+ if (plugin_unload (ctx, name, &e))
+ respond_to_user (ctx, msg, "plugin `%s' unloaded", name);
+ else
+ {
+ respond_to_user (ctx, msg, "plugin `%s' could not be unloaded: %s",
+ name, e->message);
+ error_free (e);
+ }
+}
+
+static void
+process_plugin_reload (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ // XXX: we might want to wait until the plugin terminates before we try
+ // to reload it (so that it can save its configuration or whatever)
+
+ // So far the only error that can occur is that the plugin hasn't been
+ // loaded, which in this case doesn't really matter.
+ plugin_unload (ctx, name, NULL);
+
+ process_plugin_load (ctx, msg, name);
+}
+
+static char *
+make_status_report (struct bot_context *ctx)
+{
+ struct str report = str_make ();
+ const char *reason = getenv (g_startup_reason_str);
+ if (!reason)
+ reason = "launched normally";
+ str_append_printf (&report, "\x02startup reason:\x0f %s", reason);
+
+ size_t zombies = 0;
+ const char *prepend = "; \x02plugins:\x0f ";
+ for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next)
+ {
+ if (plugin->is_zombie)
+ zombies++;
+ else
+ {
+ str_append_printf (&report, "%s%s", prepend, plugin->name);
+ prepend = ", ";
+ }
+ }
+ if (!ctx->plugins)
+ str_append_printf (&report, "%s\x02none\x0f", prepend);
+
+ str_append_printf (&report, "; \x02zombies:\x0f %zu", zombies);
+ return str_steal (&report);
+}
+
+static void
+process_privmsg (struct bot_context *ctx, const struct irc_message *msg)
+{
+ if (!is_sent_by_admin (ctx, msg))
+ return;
+ if (msg->params.len < 2)
+ return;
+
+ const char *prefix = str_map_find (&ctx->config, "prefix");
+ hard_assert (prefix != NULL); // We have a default value for this
+
+ // For us to recognize the command, it has to start with the prefix,
+ // with the exception of PM's sent directly to us.
+ const char *text = msg->params.vector[1];
+ if (!strncmp (text, prefix, strlen (prefix)))
+ text += strlen (prefix);
+ else if (!is_private_message (msg))
+ return;
+
+ const char *following;
+ struct strv list = strv_make ();
+
+ if (parse_bot_command (text, "quote", &following))
+ // This seems to replace tons of random stupid commands
+ irc_send (ctx, "%s", following);
+ else if (parse_bot_command (text, "quit", &following))
+ {
+ // We actually need this command (instead of just `quote') because we
+ // could try to reconnect to the server automatically otherwise.
+ if (*following)
+ irc_send (ctx, "QUIT :%s", following);
+ else
+ irc_send (ctx, "QUIT");
+ initiate_quit (ctx);
+ }
+ else if (parse_bot_command (text, "status", &following))
+ {
+ char *report = make_status_report (ctx);
+ respond_to_user (ctx, msg, "%s", report);
+ free (report);
+ }
+ else if (parse_bot_command (text, "load", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_load (ctx, msg, list.vector[i]);
+ }
+ else if (parse_bot_command (text, "reload", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_reload (ctx, msg, list.vector[i]);
+ }
+ else if (parse_bot_command (text, "unload", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_unload (ctx, msg, list.vector[i]);
+ }
+
+ strv_free (&list);
+}
+
+static void
+irc_forward_message_to_plugins (struct bot_context *ctx, const char *raw)
+{
+ // For consistency with plugin_process_message()
+ if (!ctx->irc_registered)
+ return;
+
+ for (struct plugin *plugin = ctx->plugins;
+ plugin; plugin = plugin->next)
+ {
+ if (plugin->is_zombie)
+ continue;
+
+ if (plugin->initialized)
+ plugin_send (plugin, "%s", raw);
+ else
+ // TODO: make sure that this buffer doesn't get too large either
+ str_append_printf (&plugin->queued_output, "%s\r\n", raw);
+ }
+}
+
+static void
+irc_process_message (const struct irc_message *msg,
+ const char *raw, void *user_data)
+{
+ struct bot_context *ctx = user_data;
+ if (g_debug_mode)
+ fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw);
+
+ // This should be as minimal as possible, I don't want to have the whole bot
+ // written in C, especially when I have this overengineered plugin system.
+ // Therefore the very basic functionality only.
+ //
+ // I should probably even rip out the autojoin...
+
+ irc_forward_message_to_plugins (ctx, raw);
+
+ if (!strcasecmp (msg->command, "PING"))
+ {
+ if (msg->params.len)
+ irc_send (ctx, "PONG :%s", msg->params.vector[0]);
+ else
+ irc_send (ctx, "PONG");
+ }
+ else if (!ctx->irc_registered && !strcasecmp (msg->command, "001"))
+ {
+ print_status ("successfully connected");
+ ctx->irc_registered = true;
+
+ const char *autojoin = str_map_find (&ctx->config, "autojoin");
+ if (autojoin)
+ irc_send (ctx, "JOIN :%s", autojoin);
+ }
+ else if (!strcasecmp (msg->command, "PRIVMSG"))
+ process_privmsg (ctx, msg);
+}
+
+enum irc_read_result
+{
+ IRC_READ_OK, ///< Some data were read successfully
+ IRC_READ_EOF, ///< The server has closed connection
+ IRC_READ_AGAIN, ///< No more data at the moment
+ IRC_READ_ERROR ///< General connection failure
+};
+
+static enum irc_read_result
+irc_fill_read_buffer_tls (struct bot_context *ctx, struct str *buf)
+{
+ int n_read;
+start:
+ ERR_clear_error ();
+ n_read = SSL_read (ctx->ssl, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (ctx->ssl, n_read, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ buf->str[buf->len += n_read] = '\0';
+ return IRC_READ_OK;
+ case SSL_ERROR_ZERO_RETURN:
+ return IRC_READ_EOF;
+ case SSL_ERROR_WANT_READ:
+ return IRC_READ_AGAIN;
+ case SSL_ERROR_WANT_WRITE:
+ {
+ // Let it finish the handshake as we don't poll for writability;
+ // any errors are to be collected by SSL_read() in the next iteration
+ struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT };
+ soft_assert (poll (&pfd, 1, 0) > 0);
+ goto start;
+ }
+ case XSSL_ERROR_TRY_AGAIN:
+ goto start;
+ default:
+ print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
+ return IRC_READ_ERROR;
+ }
+}
+
+static enum irc_read_result
+irc_fill_read_buffer (struct bot_context *ctx, struct str *buf)
+{
+ ssize_t n_read;
+start:
+ n_read = recv (ctx->irc_fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */, 0);
+
+ if (n_read > 0)
+ {
+ buf->str[buf->len += n_read] = '\0';
+ return IRC_READ_OK;
+ }
+ if (n_read == 0)
+ return IRC_READ_EOF;
+
+ if (errno == EAGAIN)
+ return IRC_READ_AGAIN;
+ if (errno == EINTR)
+ goto start;
+
+ print_debug ("%s: %s: %s", __func__, "recv", strerror (errno));
+ return IRC_READ_ERROR;
+}
+
+static bool irc_connect (struct bot_context *, struct error **);
+static void irc_queue_reconnect (struct bot_context *);
+
+static void
+irc_cancel_timers (struct bot_context *ctx)
+{
+ poller_timer_reset (&ctx->timeout_tmr);
+ poller_timer_reset (&ctx->ping_tmr);
+ poller_timer_reset (&ctx->reconnect_tmr);
+}
+
+static void
+on_irc_reconnect_timeout (void *user_data)
+{
+ struct bot_context *ctx = user_data;
+
+ struct error *e = NULL;
+ if (irc_connect (ctx, &e))
+ {
+ // TODO: inform plugins about the new connection
+ return;
+ }
+
+ print_error ("%s", e->message);
+ error_free (e);
+ irc_queue_reconnect (ctx);
+}
+
+static void
+irc_queue_reconnect (struct bot_context *ctx)
+{
+ hard_assert (ctx->irc_fd == -1);
+ print_status ("trying to reconnect in %ld seconds...",
+ ctx->reconnect_delay);
+ poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000);
+}
+
+static void
+on_irc_disconnected (struct bot_context *ctx)
+{
+ // Get rid of the dead socket and related things
+ if (ctx->ssl)
+ {
+ SSL_free (ctx->ssl);
+ ctx->ssl = NULL;
+ SSL_CTX_free (ctx->ssl_ctx);
+ ctx->ssl_ctx = NULL;
+ }
+
+ poller_fd_reset (&ctx->irc_event);
+ xclose (ctx->irc_fd);
+ ctx->irc_fd = -1;
+ ctx->irc_registered = false;
+
+ // TODO: inform plugins about the disconnect event
+
+ // All of our timers have lost their meaning now
+ irc_cancel_timers (ctx);
+
+ if (ctx->quitting)
+ try_finish_quit (ctx);
+ else if (!ctx->reconnect)
+ initiate_quit (ctx);
+ else
+ irc_queue_reconnect (ctx);
+}
+
+static void
+on_irc_ping_timeout (void *user_data)
+{
+ struct bot_context *ctx = user_data;
+ print_error ("connection timeout");
+ on_irc_disconnected (ctx);
+}
+
+static void
+on_irc_timeout (void *user_data)
+{
+ // Provoke a response from the server
+ struct bot_context *ctx = user_data;
+ irc_send (ctx, "PING :%s",
+ (char *) str_map_find (&ctx->config, "nickname"));
+}
+
+static void
+irc_reset_connection_timeouts (struct bot_context *ctx)
+{
+ irc_cancel_timers (ctx);
+ poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000);
+ poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000);
+}
+
+static void
+on_irc_readable (const struct pollfd *fd, struct bot_context *ctx)
+{
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ (void) set_blocking (ctx->irc_fd, false);
+
+ struct str *buf = &ctx->read_buffer;
+ enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *)
+ = ctx->ssl
+ ? irc_fill_read_buffer_tls
+ : irc_fill_read_buffer;
+ bool disconnected = false;
+ while (true)
+ {
+ str_reserve (buf, 512);
+ switch (fill_buffer (ctx, buf))
+ {
+ case IRC_READ_AGAIN:
+ goto end;
+ case IRC_READ_ERROR:
+ print_error ("reading from the IRC server failed");
+ disconnected = true;
+ goto end;
+ case IRC_READ_EOF:
+ print_status ("the IRC server closed the connection");
+ disconnected = true;
+ goto end;
+ case IRC_READ_OK:
+ break;
+ }
+
+ if (buf->len >= (1 << 20))
+ {
+ print_error ("the IRC server seems to spew out data frantically");
+ irc_shutdown (ctx);
+ goto end;
+ }
+ }
+end:
+ (void) set_blocking (ctx->irc_fd, true);
+ irc_process_buffer (buf, irc_process_message, ctx);
+
+ if (disconnected)
+ on_irc_disconnected (ctx);
+ else
+ irc_reset_connection_timeouts (ctx);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The bot is currently mostly synchronous (which also makes it shorter),
+// however our current SOCKS code is not, hence we must wrap it.
+
+struct irc_socks_data
+{
+ struct bot_context *ctx; ///< Bot context
+ struct poller inner_poller; ///< Special inner poller
+ bool polling; ///< Inner poller is no longer needed
+ struct socks_connector connector; ///< SOCKS connector
+ bool succeeded; ///< Were we successful in connecting?
+};
+
+static void
+irc_on_socks_connected (void *user_data, int socket, const char *hostname)
+{
+ (void) hostname;
+
+ struct irc_socks_data *data = user_data;
+ data->ctx->irc_fd = socket;
+ data->succeeded = true;
+ data->polling = false;
+}
+
+static void
+irc_on_socks_failure (void *user_data)
+{
+ struct irc_socks_data *data = user_data;
+ data->succeeded = false;
+ data->polling = false;
+}
+
+static void
+irc_on_socks_connecting (void *user_data,
+ const char *address, const char *via, const char *version)
+{
+ (void) user_data;
+ print_status ("connecting to %s via %s (%s)...", address, via, version);
+}
+
+static void
+irc_on_socks_error (void *user_data, const char *error)
+{
+ (void) user_data;
+ print_error ("%s: %s", "SOCKS connection failed", error);
+}
+
+static bool
+irc_establish_connection_socks (struct bot_context *ctx,
+ const char *socks_host, const char *socks_port,
+ const char *host, const char *service, struct error **e)
+{
+ struct irc_socks_data data;
+ struct poller *poller = &data.inner_poller;
+ struct socks_connector *connector = &data.connector;
+
+ data.ctx = ctx;
+ poller_init (poller);
+ data.polling = true;
+ socks_connector_init (connector, poller);
+ data.succeeded = false;
+
+ connector->on_connected = irc_on_socks_connected;
+ connector->on_connecting = irc_on_socks_connecting;
+ connector->on_error = irc_on_socks_error;
+ connector->on_failure = irc_on_socks_failure;
+ connector->user_data = &data;
+
+ if (socks_connector_add_target (connector, host, service, e))
+ {
+ socks_connector_run (connector, socks_host, socks_port,
+ str_map_find (&ctx->config, "socks_username"),
+ str_map_find (&ctx->config, "socks_password"));
+ while (data.polling)
+ poller_run (poller);
+ if (!data.succeeded)
+ error_set (e, "connection failed");
+ }
+
+ socks_connector_free (connector);
+ poller_free (poller);
+ return data.succeeded;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+irc_connect (struct bot_context *ctx, struct error **e)
+{
+ const char *irc_host = str_map_find (&ctx->config, "irc_host");
+ const char *irc_port = str_map_find (&ctx->config, "irc_port");
+ const char *socks_host = str_map_find (&ctx->config, "socks_host");
+ const char *socks_port = str_map_find (&ctx->config, "socks_port");
+
+ const char *nickname = str_map_find (&ctx->config, "nickname");
+ const char *username = str_map_find (&ctx->config, "username");
+ const char *realname = str_map_find (&ctx->config, "realname");
+
+ // We have a default value for these
+ hard_assert (irc_port && socks_port);
+ hard_assert (nickname && username && realname);
+
+ // TODO: again, get rid of `struct error' in here. The question is: how
+ // do we tell our caller that he should not try to reconnect?
+ if (!irc_host)
+ return error_set (e, "no hostname specified in configuration");
+
+ bool use_tls;
+ if (!irc_get_boolean_from_config (ctx, "tls", &use_tls, e))
+ return false;
+
+ bool connected = socks_host
+ ? irc_establish_connection_socks (ctx,
+ socks_host, socks_port, irc_host, irc_port, e)
+ : irc_establish_connection (ctx, irc_host, irc_port, e);
+ if (!connected)
+ return false;
+
+ if (use_tls && !irc_initialize_tls (ctx, e))
+ {
+ xclose (ctx->irc_fd);
+ ctx->irc_fd = -1;
+ return false;
+ }
+ print_status ("connection established");
+
+ ctx->irc_event = poller_fd_make (&ctx->poller, ctx->irc_fd);
+ ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable;
+ ctx->irc_event.user_data = ctx;
+
+ // TODO: in exec try: 1/ set blocking, 2/ setsockopt() SO_LINGER,
+ // (struct linger) { .l_onoff = true; .l_linger = 1 /* 1s should do */; }
+ // 3/ /* O_CLOEXEC */ But only if the QUIT message proves unreliable.
+ poller_fd_set (&ctx->irc_event, POLLIN);
+ irc_reset_connection_timeouts (ctx);
+
+ irc_send (ctx, "NICK %s", nickname);
+ irc_send (ctx, "USER %s 8 * :%s", username, realname);
+ return true;
+}
+
+static bool
+parse_config (struct bot_context *ctx, struct error **e)
+{
+ if (!irc_get_boolean_from_config (ctx, "reconnect", &ctx->reconnect, e))
+ return false;
+
+ const char *delay_str = str_map_find (&ctx->config, "reconnect_delay");
+ hard_assert (delay_str != NULL); // We have a default value for this
+ if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10))
+ {
+ return error_set (e,
+ "invalid configuration value for `%s'", "reconnect_delay");
+ }
+
+ hard_assert (!ctx->admin_re);
+ const char *admin = str_map_find (&ctx->config, "admin");
+ if (!admin)
+ return true;
+
+ struct error *error = NULL;
+ ctx->admin_re = regex_compile (admin, REG_EXTENDED | REG_NOSUB, &error);
+ if (!error)
+ return true;
+
+ error_set (e, "invalid configuration value for `%s': %s",
+ "admin", error->message);
+ error_free (error);
+ return false;
+}
+
+static void
+on_plugin_death (struct plugin *plugin, int status)
+{
+ struct bot_context *ctx = plugin->ctx;
+
+ // TODO: callbacks on children death, so that we may tell the user
+ // "plugin `name' died"; use `status'
+ if (!plugin->is_zombie && WIFSIGNALED (status))
+ {
+ const char *notes = "";
+#ifdef WCOREDUMP
+ if (WCOREDUMP (status))
+ notes = " (core dumped)";
+#endif
+ print_warning ("Plugin `%s' died from signal %d%s",
+ plugin->name, WTERMSIG (status), notes);
+ }
+
+ // Let's go through the zombie state to simplify things a bit
+ // TODO: might not be a completely bad idea to restart the plugin
+ plugin_zombify (plugin);
+
+ plugin->pid = -1;
+
+ // In theory we could close `read_fd', set `read_event->closed' to true
+ // and expect epoll to no longer return events for the descriptor, as
+ // all the pipe ends should be closed by then (the child is dead, so its
+ // pipe FDs have been closed [assuming it hasn't forked without closing
+ // the descriptors, which would be evil], and we would have closed all
+ // of our FDs for this pipe as well). In practice that doesn't work.
+ poller_fd_reset (&plugin->read_event);
+
+ xclose (plugin->read_fd);
+ plugin->read_fd = -1;
+
+ LIST_UNLINK (ctx->plugins, plugin);
+ plugin_destroy (plugin);
+
+ // Living child processes block us from quitting
+ try_finish_quit (ctx);
+}
+
+static bool
+try_reap_plugin (struct bot_context *ctx)
+{
+ int status;
+ pid_t zombie = waitpid (-1, &status, WNOHANG);
+
+ if (zombie == -1)
+ {
+ // No children to wait on
+ if (errno == ECHILD)
+ return false;
+
+ hard_assert (errno == EINTR);
+ return true;
+ }
+
+ if (zombie == 0)
+ return false;
+
+ struct plugin *plugin = plugin_find_by_pid (ctx, zombie);
+ // XXX: re-exec if something has died that we don't recognize?
+ if (soft_assert (plugin != NULL))
+ on_plugin_death (plugin, status);
+ return true;
+}
+
+static void
+kill_all_zombies (struct bot_context *ctx)
+{
+ for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next)
+ {
+ if (!plugin->is_zombie)
+ continue;
+
+ print_status ("forcefully killing a zombie of `%s' (PID %d)",
+ plugin->name, (int) plugin->pid);
+ kill (plugin->pid, SIGKILL);
+ }
+}
+
+static void
+on_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx)
+{
+ char dummy;
+ (void) read (fd->fd, &dummy, 1);
+
+ if (g_termination_requested)
+ {
+ g_termination_requested = false;
+ if (!ctx->quitting)
+ {
+ // There may be a timer set to reconnect to the server
+ irc_cancel_timers (ctx);
+
+ if (ctx->irc_fd != -1)
+ irc_send (ctx, "QUIT :Terminated by signal");
+ initiate_quit (ctx);
+ }
+ else
+ // Disregard proper termination, just kill all the children
+ kill_all_zombies (ctx);
+ }
+
+ // Reap all dead children (since the signal pipe may overflow etc. we run
+ // waitpid() in a loop to return all the zombies it knows about).
+ while (try_reap_plugin (ctx))
+ ;
+}
+
+int
+main (int argc, char *argv[])
+{
+ g_original_argv = strv_make ();
+ strv_append_vector (&g_original_argv, argv);
+
+ static const struct opt opts[] =
+ {
+ { 'd', "debug", NULL, 0, "run in debug mode" },
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ { 'w', "write-default-cfg", "FILENAME",
+ OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
+ "write a default configuration file and exit" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh =
+ opt_handler_make (argc, argv, opts, NULL, "Modular IRC bot.");
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'd':
+ g_debug_mode = true;
+ break;
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ case 'w':
+ call_simple_config_write_default (optarg, g_config_table);
+ exit (EXIT_SUCCESS);
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ opt_handler_free (&oh);
+
+ print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
+ setup_signal_handlers ();
+ init_openssl ();
+
+ struct bot_context ctx;
+ bot_context_init (&ctx);
+
+ struct error *e = NULL;
+ if (!simple_config_update_from_file (&ctx.config, &e)
+ || !setup_recovery_handler (&ctx, &e))
+ {
+ print_error ("%s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ ctx.signal_event = poller_fd_make (&ctx.poller, g_signal_pipe[0]);
+ ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
+ ctx.signal_event.user_data = &ctx;
+ poller_fd_set (&ctx.signal_event, POLLIN);
+
+#if OpenBSD >= 201605
+ // cpath is for creating the plugin home directory
+ if (pledge ("stdio rpath cpath inet proc exec", NULL))
+ exit_fatal ("%s: %s", "pledge", strerror (errno));
+#endif
+
+ plugin_load_all_from_config (&ctx);
+ if (!parse_config (&ctx, &e)
+ || !irc_connect (&ctx, &e))
+ {
+ print_error ("%s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ // TODO: clean re-exec support; to save the state I can either use argv,
+ // argp, or I can create a temporary file, unlink it and use the FD
+ // (mkstemp() on a `struct str' constructed from XDG_RUNTIME_DIR, TMPDIR
+ // or /tmp as a last resort + PROGRAM_NAME + ".XXXXXX" -> unlink();
+ // remember to use O_CREAT | O_EXCL). The state needs to be versioned.
+ // Unfortunately I cannot de/serialize SSL state.
+
+ ctx.polling = true;
+ while (ctx.polling)
+ poller_run (&ctx.poller);
+
+ bot_context_free (&ctx);
+ strv_free (&g_original_argv);
+ return EXIT_SUCCESS;
+}
+
diff --git a/xC-gen-proto-c.awk b/xC-gen-proto-c.awk
new file mode 100644
index 0000000..2810c96
--- /dev/null
+++ b/xC-gen-proto-c.awk
@@ -0,0 +1,325 @@
+# xC-gen-proto-c.awk: C backend for xC-gen-proto.awk.
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# Neither *_new() nor *_destroy() functions are provided, because they'd only
+# be useful for top-levels, and are merely extra malloc()/free() calls.
+# Users are expected to reuse buffers.
+#
+# Similarly, no constructors are produced--those are easy to write manually.
+#
+# All arrays are deserialized zero-terminated, so u8<> and i8<> can be directly
+# used as C strings.
+#
+# All types must be able to dispose partially zero values going from the back,
+# i.e., in the reverse order of deserialization.
+
+function define_internal(name, ctype) {
+ Types[name] = "internal"
+ CodegenCType[name] = ctype
+}
+
+function define_int(shortname, ctype) {
+ define_internal(shortname, ctype)
+ CodegenSerialize[shortname] = \
+ "\tstr_pack_" shortname "(w, %s);\n"
+ CodegenDeserialize[shortname] = \
+ "\tif (!msg_unpacker_" shortname "(r, &%s))\n" \
+ "\t\treturn false;\n"
+}
+
+function define_sint(size) { define_int("i" size, "int" size "_t") }
+function define_uint(size) { define_int("u" size, "uint" size "_t") }
+
+function codegen_begin() {
+ define_sint("8")
+ define_sint("16")
+ define_sint("32")
+ define_sint("64")
+ define_uint("8")
+ define_uint("16")
+ define_uint("32")
+ define_uint("64")
+
+ define_internal("string", "struct str")
+ CodegenDispose["string"] = "\tstr_free(&%s);\n"
+ CodegenSerialize["string"] = \
+ "\tif (!proto_string_serialize(&%s, w))\n" \
+ "\t\treturn false;\n"
+ CodegenDeserialize["string"] = \
+ "\tif (!proto_string_deserialize(&%s, r))\n" \
+ "\t\treturn false;\n"
+
+ define_internal("bool", "bool")
+ CodegenSerialize["bool"] = \
+ "\tstr_pack_u8(w, !!%s);\n"
+ CodegenDeserialize["bool"] = \
+ "\t{\n" \
+ "\t\tuint8_t v = 0;\n" \
+ "\t\tif (!msg_unpacker_u8(r, &v))\n" \
+ "\t\t\treturn false;\n" \
+ "\t\t%s = !!v;\n" \
+ "\t}\n"
+
+ print "// This file directly depends on liberty.c, but doesn't include it."
+
+ print ""
+ print "static bool"
+ print "proto_string_serialize(const struct str *s, struct str *w) {"
+ print "\tif (s->len > UINT32_MAX)"
+ print "\t\treturn false;"
+ print "\tstr_pack_u32(w, s->len);"
+ print "\tstr_append_str(w, s);"
+ print "\treturn true;"
+ print "}"
+
+ print ""
+ print "static bool"
+ print "proto_string_deserialize(struct str *s, struct msg_unpacker *r) {"
+ print "\tuint32_t len = 0;"
+ print "\tif (!msg_unpacker_u32(r, &len))"
+ print "\t\treturn false;"
+ print "\tif (msg_unpacker_get_available(r) < len)"
+ print "\t\treturn false;"
+ print "\t*s = str_make();"
+ print "\tstr_append_data(s, r->data + r->offset, len);"
+ print "\tr->offset += len;"
+ print "\tif (!utf8_validate (s->str, s->len))"
+ print "\t\treturn false;"
+ print "\treturn true;"
+ print "}"
+}
+
+function codegen_constant(name, value) {
+ print ""
+ print "enum { " PrefixUpper name " = " value " };"
+}
+
+function codegen_enum_value(name, subname, value, cg) {
+ append(cg, "fields",
+ "\t" PrefixUpper toupper(cameltosnake(name)) "_" subname \
+ " = " value ",\n")
+}
+
+function codegen_enum(name, cg, ctype) {
+ ctype = "enum " PrefixLower cameltosnake(name)
+ print ""
+ print ctype " {"
+ print cg["fields"] "};"
+
+ # XXX: This should also check if it isn't out-of-range for any reason,
+ # but our usage of sprintf() stands in the way a bit.
+ CodegenSerialize[name] = "\tstr_pack_i8(w, %s);\n"
+ CodegenDeserialize[name] = \
+ "\t{\n" \
+ "\t\tint8_t v = 0;\n" \
+ "\t\tif (!msg_unpacker_i8(r, &v) || !v)\n" \
+ "\t\t\treturn false;\n" \
+ "\t\t%s = v;\n" \
+ "\t}\n"
+
+ CodegenCType[name] = ctype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_struct_tag(d, cg, f) {
+ f = "self->" d["name"]
+ append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
+ append(cg, "dispose", sprintf(CodegenDispose[d["type"]], f))
+ append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
+ # Do not deserialize here, that would be out of order.
+}
+
+function codegen_struct_field(d, cg, f, dispose, serialize, deserialize) {
+ f = "self->" d["name"]
+ dispose = CodegenDispose[d["type"]]
+ serialize = CodegenSerialize[d["type"]]
+ deserialize = CodegenDeserialize[d["type"]]
+ if (!d["isarray"]) {
+ append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
+ append(cg, "dispose", sprintf(dispose, f))
+ append(cg, "serialize", sprintf(serialize, f))
+ append(cg, "deserialize", sprintf(deserialize, f))
+ return
+ }
+
+ append(cg, "fields",
+ "\t" CodegenCType["u32"] " " d["name"] "_len;\n" \
+ "\t" CodegenCType[d["type"]] " *" d["name"] ";\n")
+
+ if (dispose)
+ append(cg, "dispose", "\tif (" f ")\n" \
+ "\t\tfor (size_t i = 0; i < " f "_len; i++)\n" \
+ indent(indent(sprintf(dispose, f "[i]"))))
+ append(cg, "dispose", "\tfree(" f ");\n")
+
+ append(cg, "serialize", sprintf(CodegenSerialize["u32"], f "_len"))
+ if (d["type"] == "u8" || d["type"] == "i8") {
+ append(cg, "serialize",
+ "\tstr_append_data(w, " f ", " f "_len);\n")
+ } else if (serialize) {
+ append(cg, "serialize",
+ "\tfor (size_t i = 0; i < " f "_len; i++)\n" \
+ indent(sprintf(serialize, f "[i]")))
+ }
+
+ append(cg, "deserialize", sprintf(CodegenDeserialize["u32"], f "_len") \
+ "\tif (!(" f " = calloc(" f "_len + 1, sizeof *" f ")))\n" \
+ "\t\treturn false;\n")
+ if (d["type"] == "u8" || d["type"] == "i8") {
+ append(cg, "deserialize",
+ "\tif (msg_unpacker_get_available(r) < " f "_len)\n" \
+ "\t\treturn false;\n" \
+ "\tmemcpy(" f ", r->data + r->offset, " f "_len);\n" \
+ "\tr->offset += " f "_len;\n")
+ } else if (deserialize) {
+ append(cg, "deserialize",
+ "\tfor (size_t i = 0; i < " f "_len; i++)\n" \
+ indent(sprintf(deserialize, f "[i]")))
+ }
+}
+
+function codegen_struct(name, cg, ctype, funcname) {
+ ctype = "struct " PrefixLower cameltosnake(name)
+ print ""
+ print ctype " {"
+ print cg["fields"] "};"
+
+ if (cg["dispose"]) {
+ funcname = PrefixLower cameltosnake(name) "_free"
+ print ""
+ print "static void\n" funcname "(" ctype " *self) {"
+ print cg["dispose"] "}"
+
+ CodegenDispose[name] = "\t" funcname "(&%s);\n"
+ }
+ if (cg["serialize"]) {
+ funcname = PrefixLower cameltosnake(name) "_serialize"
+ print ""
+ print "static bool\n" \
+ funcname "(\n\t\t" ctype " *self, struct str *w) {"
+ print cg["serialize"] "\treturn true;"
+ print "}"
+
+ CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
+ "\t\treturn false;\n"
+ }
+ if (cg["deserialize"]) {
+ funcname = PrefixLower cameltosnake(name) "_deserialize"
+ print ""
+ print "static bool\n" \
+ funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
+ print cg["deserialize"] "\treturn true;"
+ print "}"
+
+ CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
+ "\t\treturn false;\n"
+ }
+
+ CodegenCType[name] = ctype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_union_tag(d, cg) {
+ cg["tagtype"] = d["type"]
+ cg["tagname"] = d["name"]
+ append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
+}
+
+function codegen_union_struct( \
+ name, casename, cg, scg, structname, fieldname, fullcasename) {
+ # Don't generate obviously useless structs.
+ fullcasename = toupper(cameltosnake(cg["tagtype"])) "_" casename
+ if (!scg["dispose"] && !scg["deserialize"]) {
+ append(cg, "structless", "\tcase " PrefixUpper fullcasename ":\n")
+ for (i in scg)
+ delete scg[i]
+ return
+ }
+
+ # And thus not all generated structs are present in Types.
+ structname = name "_" casename
+ fieldname = tolower(casename)
+ codegen_struct(structname, scg)
+
+ append(cg, "fields", "\t" CodegenCType[structname] " " fieldname ";\n")
+ if (CodegenDispose[structname])
+ append(cg, "dispose", "\tcase " PrefixUpper fullcasename ":\n" \
+ indent(sprintf(CodegenDispose[structname], "self->" fieldname)) \
+ "\t\tbreak;\n")
+
+ # With no de/serialization code, this will simply recognize the tag.
+ append(cg, "serialize", "\tcase " PrefixUpper fullcasename ":\n" \
+ indent(sprintf(CodegenSerialize[structname], "self->" fieldname)) \
+ "\t\tbreak;\n")
+ append(cg, "deserialize", "\tcase " PrefixUpper fullcasename ":\n" \
+ indent(sprintf(CodegenDeserialize[structname], "self->" fieldname)) \
+ "\t\tbreak;\n")
+}
+
+function codegen_union(name, cg, f, ctype, funcname) {
+ ctype = "union " PrefixLower cameltosnake(name)
+ print ""
+ print ctype " {"
+ print cg["fields"] "};"
+
+ f = "self->" cg["tagname"]
+ if (cg["dispose"]) {
+ funcname = PrefixLower cameltosnake(name) "_free"
+ print ""
+ print "static void\n" funcname "(" ctype " *self) {"
+ print "\tswitch (" f ") {"
+ if (cg["structless"])
+ print cg["structless"] \
+ indent(sprintf(CodegenDispose[cg["tagtype"]], f)) "\t\tbreak;"
+ print cg["dispose"] "\tdefault:"
+ print "\t\tbreak;"
+ print "\t}"
+ print "}"
+
+ CodegenDispose[name] = "\t" funcname "(&%s);\n"
+ }
+ if (cg["serialize"]) {
+ funcname = PrefixLower cameltosnake(name) "_serialize"
+ print ""
+ print "static bool\n" \
+ funcname "(\n\t\t" ctype " *self, struct str *w) {"
+ print "\tswitch (" f ") {"
+ if (cg["structless"])
+ print cg["structless"] \
+ indent(sprintf(CodegenSerialize[cg["tagtype"]], f)) "\t\tbreak;"
+ print cg["serialize"] "\tdefault:"
+ print "\t\treturn false;"
+ print "\t}"
+ print "\treturn true;"
+ print "}"
+
+ CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
+ "\t\treturn false;\n"
+ }
+ if (cg["deserialize"]) {
+ funcname = PrefixLower cameltosnake(name) "_deserialize"
+ print ""
+ print "static bool\n" \
+ funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
+ print sprintf(CodegenDeserialize[cg["tagtype"]], f)
+ print "\tswitch (" f ") {"
+ if (cg["structless"])
+ print cg["structless"] "\t\tbreak;"
+ print cg["deserialize"] "\tdefault:"
+ print "\t\treturn false;"
+ print "\t}"
+ print "\treturn true;"
+ print "}"
+
+ CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
+ "\t\treturn false;\n"
+ }
+
+ CodegenCType[name] = ctype
+ for (i in cg)
+ delete cg[i]
+}
diff --git a/xC-gen-proto-go.awk b/xC-gen-proto-go.awk
new file mode 100644
index 0000000..477a471
--- /dev/null
+++ b/xC-gen-proto-go.awk
@@ -0,0 +1,519 @@
+# xC-gen-proto-go.awk: Go backend for xC-gen-proto.awk.
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# This backend also enables proxying to other endpoints using JSON.
+
+function define_internal(name, gotype) {
+ Types[name] = "internal"
+ CodegenGoType[name] = gotype
+}
+
+function define_sint(size, shortname, gotype) {
+ shortname = "i" size
+ gotype = "int" size
+ define_internal(shortname, gotype)
+
+ CodegenAppendJSON[shortname] = \
+ "\tb = strconv.AppendInt(b, int64(%s), 10)\n"
+ if (size == 8) {
+ CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= 1 {\n" \
+ "\t\t%s, data = int8(data[0]), data[1:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+ return
+ }
+
+ CodegenSerialize[shortname] = \
+ "\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= " (size / 8) " {\n" \
+ "\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \
+ "\t\tdata = data[" (size / 8) ":]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+}
+
+function define_uint(size, shortname, gotype) {
+ # Both []byte and []uint8 luckily marshal as base64-encoded JSON strings,
+ # so there's no need to rename the type as an exception.
+ shortname = "u" size
+ gotype = "uint" size
+ define_internal(shortname, gotype)
+
+ CodegenAppendJSON[shortname] = \
+ "\tb = strconv.AppendUint(b, uint64(%s), 10)\n"
+ if (size == 8) {
+ CodegenSerialize[shortname] = "\tdata = append(data, %s)\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= 1 {\n" \
+ "\t\t%s, data = data[0], data[1:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+ return
+ }
+
+ CodegenSerialize[shortname] = \
+ "\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= " (size / 8) " {\n" \
+ "\t\t%s = binary.BigEndian.Uint" size "(data)\n" \
+ "\t\tdata = data[" (size / 8) ":]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+}
+
+function codegen_begin() {
+ define_sint("8")
+ define_sint("16")
+ define_sint("32")
+ define_sint("64")
+ define_uint("8")
+ define_uint("16")
+ define_uint("32")
+ define_uint("64")
+
+ define_internal("bool", "bool")
+ CodegenAppendJSON["bool"] = \
+ "\tb = strconv.AppendBool(b, %s)\n"
+ CodegenSerialize["bool"] = \
+ "\tif %s {\n" \
+ "\t\tdata = append(data, 1)\n" \
+ "\t} else {\n" \
+ "\t\tdata = append(data, 0)\n" \
+ "\t}\n"
+ CodegenDeserialize["bool"] = \
+ "\tif data, ok = protoConsumeBoolFrom(data, &%s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ define_internal("string", "string")
+ CodegenSerialize["string"] = \
+ "\tif data, ok = protoAppendStringTo(data, %s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ CodegenDeserialize["string"] = \
+ "\tif data, ok = protoConsumeStringFrom(data, &%s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ print "package main"
+ print ""
+ print "import ("
+ print "\t`encoding/base64`"
+ print "\t`encoding/binary`"
+ print "\t`encoding/json`"
+ print "\t`errors`"
+ print "\t`math`"
+ print "\t`strconv`"
+ print "\t`unicode/utf8`"
+ print ")"
+ print ""
+
+ print "// protoConsumeBoolFrom tries to deserialize a boolean value"
+ print "// from the beginning of a byte stream. When successful,"
+ print "// it returns a subslice with any data that might follow."
+ print "func protoConsumeBoolFrom(data []byte, b *bool) ([]byte, bool) {"
+ print "\tif len(data) < 1 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tif data[0] != 0 {"
+ print "\t\t*b = true"
+ print "\t} else {"
+ print "\t\t*b = false"
+ print "\t}"
+ print "\treturn data[1:], true"
+ print "}"
+ print ""
+
+ print "// protoAppendStringTo tries to serialize a string value,"
+ print "// appending it to the end of a byte stream."
+ print "func protoAppendStringTo(data []byte, s string) ([]byte, bool) {"
+ print "\tif len(s) > math.MaxUint32 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
+ print "\treturn append(data, s...), true"
+ print "}"
+ print ""
+
+ print "// protoConsumeStringFrom tries to deserialize a string value"
+ print "// from the beginning of a byte stream. When successful,"
+ print "// it returns a subslice with any data that might follow."
+ print "func protoConsumeStringFrom(data []byte, s *string) ([]byte, bool) {"
+ print "\tif len(data) < 4 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tlength := binary.BigEndian.Uint32(data)"
+ print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\t*s = string(data[:length])"
+ print "\tif !utf8.ValidString(*s) {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data[length:], true"
+ print "}"
+ print ""
+
+ print "// protoUnmarshalEnumJSON converts a JSON fragment to an integer,"
+ print "// ensuring that it's within the expected range of enum values."
+ print "func protoUnmarshalEnumJSON(data []byte) (int64, error) {"
+ print "\tvar n int64"
+ print "\tif err := json.Unmarshal(data, &n); err != nil {"
+ print "\t\treturn 0, err"
+ print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {"
+ print "\t\treturn 0, errors.New(`integer out of range`)"
+ print "\t} else {"
+ print "\t\treturn n, nil"
+ print "\t}"
+ print "}"
+ print ""
+}
+
+function codegen_constant(name, value) {
+ print "const " PrefixCamel snaketocamel(name) " = " value
+ print ""
+}
+
+function codegen_enum_value(name, subname, value, cg, goname) {
+ goname = PrefixCamel name snaketocamel(subname)
+ append(cg, "fields",
+ "\t" goname " = " value "\n")
+ append(cg, "stringer",
+ "\tcase " goname ":\n" \
+ "\t\treturn `" snaketocamel(subname) "`\n")
+ append(cg, "marshal",
+ goname ",\n")
+ append(cg, "unmarshal",
+ "\tcase `" snaketocamel(subname) "`:\n" \
+ "\t\t*v = " goname "\n")
+}
+
+function codegen_enum(name, cg, gotype, fields) {
+ gotype = PrefixCamel name
+ print "type " gotype " int8"
+ print ""
+
+ print "const ("
+ print cg["fields"] ")"
+ print ""
+
+ print "func (v " gotype ") String() string {"
+ print "\tswitch v {"
+ print cg["stringer"] "\tdefault:"
+ print "\t\treturn strconv.Itoa(int(v))"
+ print "\t}"
+ print "}"
+ print ""
+
+ CodegenIsMarshaler[name] = 1
+ fields = cg["marshal"]
+ sub(/,\n$/, ":", fields)
+ gsub(/\n/, "\n\t", fields)
+ print "func (v " gotype ") MarshalJSON() ([]byte, error) {"
+ print "\tswitch v {"
+ print indent("case " fields)
+ print "\t\treturn []byte(`\"` + v.String() + `\"`), nil"
+ print "\t}"
+ print "\treturn json.Marshal(int(v))"
+ print "}"
+ print ""
+
+ print "func (v *" gotype ") UnmarshalJSON(data []byte) error {"
+ print "\tvar s string"
+ print "\tif json.Unmarshal(data, &s) == nil {"
+ print "\t\t// Handled below."
+ print "\t} else if n, err := protoUnmarshalEnumJSON(data); err != nil {"
+ print "\t\treturn err"
+ print "\t} else {"
+ print "\t\t*v = " gotype "(n)"
+ print "\t\treturn nil"
+ print "\t}"
+ print ""
+ print "\tswitch s {"
+ print cg["unmarshal"] "\tdefault:"
+ print "\t\treturn errors.New(`unrecognized value: ` + s)"
+ print "\t}"
+ print "\treturn nil"
+ print "}"
+ print ""
+
+ # XXX: This should also check if it isn't out-of-range for any reason,
+ # but our usage of sprintf() stands in the way a bit.
+ CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n"
+ CodegenDeserialize[name] = \
+ "\tif len(data) >= 1 {\n" \
+ "\t\t%s, data = " gotype "(data[0]), data[1:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_marshal(type, f, marshal) {
+ if (CodegenAppendJSON[type])
+ return sprintf(CodegenAppendJSON[type], f)
+
+ # Complex types are json.Marshalers, there's no need to json.Marshal(&f).
+ if (CodegenIsMarshaler[type])
+ marshal = f ".MarshalJSON()"
+ else
+ marshal = "json.Marshal(" f ")"
+
+ return \
+ "\tif j, err := " marshal "; err != nil {\n" \
+ "\t\treturn nil, err\n" \
+ "\t} else {\n" \
+ "\t\tb = append(b, j...)\n" \
+ "\t}\n"
+}
+
+function codegen_struct_field_marshal(d, cg, camel, f, marshal) {
+ camel = snaketocamel(d["name"])
+ f = "s." camel
+ if (!d["isarray"]) {
+ append(cg, "marshal",
+ "\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \
+ codegen_marshal(d["type"], f))
+ return
+ }
+
+ # Note that we do not produce `null` for nil slices, unlike encoding/json.
+ # And arrays never get deserialized as such.
+ if (d["type"] == "u8") {
+ append(cg, "marshal",
+ "\tb = append(b, `,\"" decapitalize(camel) "\":\"`...)\n" \
+ "\tb = append(b, base64.StdEncoding.EncodeToString(" f ")...)\n" \
+ "\tb = append(b, '\"')\n")
+ return
+ }
+
+ append(cg, "marshal",
+ "\tb = append(b, `,\"" decapitalize(camel) "\":[`...)\n" \
+ "\tfor i := 0; i < len(" f "); i++ {\n" \
+ "\t\tif i > 0 {\n" \
+ "\t\t\tb = append(b, ',')\n" \
+ "\t\t}\n" \
+ indent(codegen_marshal(d["type"], f "[i]")) \
+ "\t}\n" \
+ "\tb = append(b, ']')\n")
+}
+
+function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
+ codegen_struct_field_marshal(d, cg)
+
+ camel = snaketocamel(d["name"])
+ f = "s." camel
+ serialize = CodegenSerialize[d["type"]]
+ deserialize = CodegenDeserialize[d["type"]]
+ if (!d["isarray"]) {
+ append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+ append(cg, "serialize", sprintf(serialize, f))
+ append(cg, "deserialize", sprintf(deserialize, f))
+ return
+ }
+
+ append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+
+ # XXX: This should also check if it isn't out-of-range for any reason.
+ append(cg, "serialize",
+ sprintf(CodegenSerialize["u32"], "uint32(len(" f "))"))
+ if (d["type"] == "u8") {
+ append(cg, "serialize",
+ "\tdata = append(data, " f "...)\n")
+ } else {
+ append(cg, "serialize",
+ "\tfor i := 0; i < len(" f "); i++ {\n" \
+ indent(sprintf(serialize, f "[i]")) \
+ "\t}\n")
+ }
+
+ append(cg, "deserialize",
+ "\t{\n" \
+ "\t\tvar length uint32\n" \
+ indent(sprintf(CodegenDeserialize["u32"], "length")))
+ if (d["type"] == "u8") {
+ append(cg, "deserialize",
+ "\t\tif uint64(len(data)) < uint64(length) {\n" \
+ "\t\t\treturn nil, false\n" \
+ "\t\t}\n" \
+ "\t\t" f ", data = data[:length], data[length:]\n" \
+ "\t}\n")
+ } else {
+ append(cg, "deserialize",
+ "\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \
+ "\t}\n" \
+ "\tfor i := 0; i < len(" f "); i++ {\n" \
+ indent(sprintf(deserialize, f "[i]")) \
+ "\t}\n")
+ }
+}
+
+function codegen_struct_tag(d, cg, camel, f) {
+ codegen_struct_field_marshal(d, cg)
+
+ camel = snaketocamel(d["name"])
+ f = "s." camel
+ append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+ append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
+ # Do not deserialize here, that is already done by the containing union.
+}
+
+function codegen_struct(name, cg, gotype) {
+ gotype = PrefixCamel name
+ print "type " gotype " struct {\n" cg["fields"] "}\n"
+
+ if (cg["marshal"]) {
+ CodegenIsMarshaler[name] = 1
+ print "func (s *" gotype ") MarshalJSON() ([]byte, error) {"
+ print "\tb := []byte{}"
+ print cg["marshal"] "\tb[0] = '{'"
+ print "\treturn append(b, '}'), nil"
+ print "}"
+ print ""
+ }
+
+ if (cg["serialize"]) {
+ print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print cg["serialize"] "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenSerialize[name] = \
+ "\tif data, ok = %s.AppendTo(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ }
+ if (cg["deserialize"]) {
+ print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print cg["deserialize"] "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenDeserialize[name] = \
+ "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ }
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_union_tag(d, cg) {
+ cg["tagtype"] = d["type"]
+ cg["tagname"] = d["name"]
+ # The tag is implied from the type of struct stored in the interface.
+}
+
+function codegen_union_struct(name, casename, cg, scg, structname, init) {
+ # And thus not all generated structs are present in Types.
+ structname = name snaketocamel(casename)
+ codegen_struct(structname, scg)
+
+ init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \
+ ": " decapitalize(snaketocamel(cg["tagname"])) "}"
+ append(cg, "unmarshal",
+ "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
+ "\t\ts := " init "\n" \
+ "\t\terr = json.Unmarshal(data, &s)\n" \
+ "\t\tu.Interface = &s\n")
+ append(cg, "serialize",
+ "\tcase *" CodegenGoType[structname] ":\n" \
+ indent(sprintf(CodegenSerialize[structname], "union")))
+ append(cg, "deserialize",
+ "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
+ "\t\ts := " init "\n" \
+ indent(sprintf(CodegenDeserialize[structname], "s")) \
+ "\t\tu.Interface = &s\n")
+}
+
+function codegen_union(name, cg, gotype, tagfield, tagvar) {
+ gotype = PrefixCamel name
+ print "type " gotype " struct {"
+ print "\tInterface any"
+ print "}"
+ print ""
+
+ # This cannot be a pointer method, it wouldn't work recursively.
+ CodegenIsMarshaler[name] = 1
+ print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
+ print "\treturn u.Interface.(json.Marshaler).MarshalJSON()"
+ print "}"
+ print ""
+
+ tagfield = snaketocamel(cg["tagname"])
+ tagvar = decapitalize(tagfield)
+ print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
+ print "\tvar t struct {"
+ print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \
+ " `json:\"" tagvar "\"`"
+ print "\t}"
+ print "\tif err := json.Unmarshal(data, &t); err != nil {"
+ print "\t\treturn err"
+ print "\t}"
+ print ""
+ print "\tswitch " tagvar " := t." tagfield "; " tagvar " {"
+ print cg["unmarshal"] "\tdefault:"
+ print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
+ print "\t}"
+ print "\treturn err"
+ print "}"
+ print ""
+
+ # XXX: Consider changing the interface into an AppendTo/ConsumeFrom one,
+ # that would eliminate these type case switches entirely.
+ # On the other hand, it would make it possible to send unsuitable structs.
+ print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print "\tswitch union := u.Interface.(type) {"
+ print cg["serialize"] "\tdefault:"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenSerialize[name] = \
+ "\tif data, ok = %s.AppendTo(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]]
+ print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar)
+ print "\tswitch " tagvar " {"
+ print cg["deserialize"] "\tdefault:"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenDeserialize[name] = \
+ "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}
diff --git a/xC-gen-proto-js.awk b/xC-gen-proto-js.awk
new file mode 100644
index 0000000..752fd18
--- /dev/null
+++ b/xC-gen-proto-js.awk
@@ -0,0 +1,223 @@
+# xC-gen-proto-js.awk: Javascript backend for xC-gen-proto.awk.
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# This backend is currently for decoding the binary format only.
+# (JSON is way too expensive to process and transfer.)
+#
+# Import the resulting script as a Javascript module.
+
+function define_internal(name) {
+ Types[name] = "internal"
+}
+
+function define_sint(size, shortname) {
+ shortname = "i" size
+ define_internal(shortname)
+ CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
+
+ print ""
+ print "\t" shortname "() {"
+ if (size == "64") {
+ # XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
+ print "\t\tconst " shortname \
+ " = Number(this.getBigInt" size "(this.offset))"
+ } else {
+ print "\t\tconst " shortname " = this.getInt" size "(this.offset)"
+ }
+ print "\t\tthis.offset += " (size / 8)
+ print "\t\treturn " shortname
+ print "\t}"
+}
+
+function define_uint(size, shortname) {
+ shortname = "u" size
+ define_internal(shortname)
+ CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
+
+ print ""
+ print "\t" shortname "() {"
+ if (size == "64") {
+ # XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
+ print "\t\tconst " shortname \
+ " = Number(this.getBigUint" size "(this.offset))"
+ } else {
+ print "\t\tconst " shortname " = this.getUint" size "(this.offset)"
+ }
+ print "\t\tthis.offset += " (size / 8)
+ print "\t\treturn " shortname
+ print "\t}"
+}
+
+function codegen_begin() {
+ print "export class Reader extends DataView {"
+ print "\tconstructor() {"
+ print "\t\tsuper(...arguments)"
+ print "\t\tthis.offset = 0"
+ print "\t\tthis.decoder = new TextDecoder('utf-8', {fatal: true})"
+ print "\t}"
+ print ""
+ print "\tget empty() {"
+ print "\t\treturn this.byteLength <= this.offset"
+ print "\t}"
+ print ""
+ print "\trequire(len) {"
+ print "\t\tif (this.byteLength - this.offset < len)"
+ print "\t\t\tthrow `Premature end of data`"
+ print "\t\treturn this.byteOffset + this.offset"
+ print "\t}"
+
+ define_internal("string")
+ CodegenDeserialize["string"] = "\t%s = r.string()\n"
+
+ print ""
+ print "\tstring() {"
+ print "\t\tconst len = this.getUint32(this.offset)"
+ print "\t\tthis.offset += 4"
+ print "\t\tconst array = new Uint8Array("
+ print "\t\t\tthis.buffer, this.require(len), len)"
+ print "\t\tthis.offset += len"
+ print "\t\treturn this.decoder.decode(array)"
+ print "\t}"
+
+ define_internal("bool")
+ CodegenDeserialize["bool"] = "\t%s = r.bool()\n"
+
+ print ""
+ print "\tbool() {"
+ print "\t\tconst u8 = this.getUint8(this.offset)"
+ print "\t\tthis.offset += 1"
+ print "\t\treturn u8 != 0"
+ print "\t}"
+
+ define_sint("8")
+ define_sint("16")
+ define_sint("32")
+ define_sint("64")
+ define_uint("8")
+ define_uint("16")
+ define_uint("32")
+ define_uint("64")
+
+ print "}"
+}
+
+function codegen_constant(name, value) {
+ print ""
+ print "export const " decapitalize(snaketocamel(name)) " = " value
+}
+
+function codegen_enum_value(name, subname, value, cg) {
+ append(cg, "fields", "\t" snaketocamel(subname) ": " value ",\n")
+}
+
+function codegen_enum(name, cg) {
+ print ""
+ print "export const " name " = Object.freeze({"
+ print cg["fields"] "})"
+
+ CodegenDeserialize[name] = "\t%s = r.i8()\n"
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_struct_field(d, cg, camel, f, deserialize) {
+ camel = decapitalize(snaketocamel(d["name"]))
+ f = "s." camel
+ append(cg, "fields", "\t" camel "\n")
+
+ deserialize = CodegenDeserialize[d["type"]]
+ if (!d["isarray"]) {
+ append(cg, "deserialize", sprintf(deserialize, f))
+ return
+ }
+
+ append(cg, "deserialize",
+ "\t{\n" \
+ indent(sprintf(CodegenDeserialize["u32"], "const len")))
+ if (d["type"] == "u8") {
+ append(cg, "deserialize",
+ "\t\t" f " = new Uint8Array(\n" \
+ "\t\t\tr.buffer, r.require(len), len)\n" \
+ "\t\tr.offset += len\n" \
+ "\t}\n")
+ return
+ }
+ if (d["type"] == "i8") {
+ append(cg, "deserialize",
+ "\t\t" f " = new Int8Array(\n" \
+ "\t\t\tr.buffer, r.require(len), len)\n" \
+ "\t\tr.offset += len\n" \
+ "\t}\n")
+ return
+ }
+
+ append(cg, "deserialize",
+ "\t\t" f " = new Array(len)\n" \
+ "\t}\n" \
+ "\tfor (let i = 0; i < " f ".length; i++)\n" \
+ indent(sprintf(deserialize, f "[i]")))
+}
+
+function codegen_struct_tag(d, cg) {
+ append(cg, "fields", "\t" decapitalize(snaketocamel(d["name"])) "\n")
+ # Do not deserialize here, that is already done by the containing union.
+}
+
+function codegen_struct(name, cg) {
+ print ""
+ print "export class " name " {"
+ print cg["fields"] cg["methods"]
+ print "\tstatic deserialize(r) {"
+ print "\t\tconst s = new " name "()"
+ print indent(cg["deserialize"]) "\t\treturn s"
+ print "\t}"
+ print "}"
+
+ CodegenDeserialize[name] = "\t%s = " name ".deserialize(r)\n"
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_union_tag(d, cg) {
+ cg["tagtype"] = d["type"]
+ cg["tagname"] = d["name"]
+}
+
+function codegen_union_struct(name, casename, cg, scg, structname) {
+ append(scg, "methods",
+ "\n" \
+ "\tconstructor() {\n" \
+ "\t\tthis." decapitalize(snaketocamel(cg["tagname"])) \
+ " = " cg["tagtype"] "." snaketocamel(casename) "\n" \
+ "\t}\n")
+
+ # And thus not all generated structs are present in Types.
+ structname = name snaketocamel(casename)
+ codegen_struct(structname, scg)
+
+ append(cg, "deserialize",
+ "\tcase " cg["tagtype"] "." snaketocamel(casename) ":\n" \
+ "\t{\n" \
+ indent(sprintf(CodegenDeserialize[structname], "const s")) \
+ "\t\treturn s\n" \
+ "\t}\n")
+}
+
+function codegen_union(name, cg, tagvar) {
+ tagvar = decapitalize(snaketocamel(cg["tagname"]))
+
+ print ""
+ print "export function deserialize" name "(r) {"
+ print sprintf(CodegenDeserialize[cg["tagtype"]], "const " tagvar) \
+ "\tswitch (" tagvar ") {"
+ print cg["deserialize"] "\tdefault:"
+ print "\t\tthrow `Unknown " cg["tagtype"] " (${tagvar})`"
+ print "\t}"
+ print "}"
+
+ CodegenDeserialize[name] = "\t%s = deserialize" name "(r)\n"
+ for (i in cg)
+ delete cg[i]
+}
diff --git a/xC-gen-proto.awk b/xC-gen-proto.awk
new file mode 100644
index 0000000..ad375af
--- /dev/null
+++ b/xC-gen-proto.awk
@@ -0,0 +1,305 @@
+# xC-gen-proto.awk: an XDR-derived code generator for network protocols.
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# You may read RFC 4506 for context, however it is only a source of inspiration.
+# Grammar is easy to deduce from the parser.
+#
+# Native types: bool, u{8,16,32,64}, i{8,16,32,64}, string
+#
+# Don't define any new types, unless you hate yourself, then it's okay to do so.
+# Backends tend to be a pain in the arse, for different reasons.
+#
+# All numbers are encoded in big-endian byte order.
+# Booleans are one byte each.
+# Strings must be valid UTF-8, use u8<> to lift that restriction.
+# String and array lengths are encoded as u32.
+# Enumeration values automatically start at 1, and are encoded as i8.
+# Any struct or union field may be a variable-length array.
+#
+# Message framing is done externally, but also happens to prefix u32 lengths,
+# unless this role is already filled by, e.g., WebSocket.
+#
+# Usage: env LC_ALL=C awk -f xC-gen-proto.awk -f xC-gen-proto-{c,go,js}.awk \
+# xC-proto > xC-proto.{c,go,js} | {clang-format,gofmt,...}
+
+# --- Utilities ----------------------------------------------------------------
+
+function cameltosnake(s) {
+ while (match(s, /[[:lower:]][[:upper:]]/)) {
+ s = substr(s, 1, RSTART) "_" \
+ tolower(substr(s, RSTART + 1, RLENGTH - 1)) \
+ substr(s, RSTART + RLENGTH)
+ }
+ return tolower(s)
+}
+
+function snaketocamel(s) {
+ s = toupper(substr(s, 1, 1)) tolower(substr(s, 2))
+ while (match(s, /_[[:alnum:]]/)) {
+ s = substr(s, 1, RSTART - 1) \
+ toupper(substr(s, RSTART + 1, RLENGTH - 1)) \
+ substr(s, RSTART + RLENGTH)
+ }
+ return s
+}
+
+function decapitalize(s) {
+ if (match(s, /[[:upper:]][[:lower:]]/)) {
+ return tolower(substr(s, 1, 1)) substr(s, 2)
+ }
+ return s
+}
+
+function indent(s) {
+ if (!s)
+ return s
+
+ gsub(/\n/, "\n\t", s)
+ sub(/\t*$/, "", s)
+ return "\t" s
+}
+
+function append(a, key, value) {
+ a[key] = a[key] value
+}
+
+# --- Parsing ------------------------------------------------------------------
+
+function fatal(message) {
+ print "// " FILENAME ":" FNR ": fatal error: " message
+ print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
+ exit 1
+}
+
+function skipcomment() {
+ do {
+ if (match($0, /[*][/]/)) {
+ $0 = substr($0, RSTART + RLENGTH)
+ return
+ }
+ } while (getline > 0)
+ fatal("unterminated block comment")
+}
+
+function nexttoken() {
+ do {
+ if (match($0, /^[[:space:]]+/)) {
+ $0 = substr($0, RLENGTH + 1)
+ } else if (match($0, /^[/][/].*/)) {
+ $0 = ""
+ } else if (match($0, /^[/][*]/)) {
+ $0 = substr($0, RLENGTH + 1)
+ skipcomment()
+ } else if (match($0, /^[[:alpha:]][[:alnum:]_]*/)) {
+ Token = substr($0, 1, RLENGTH)
+ $0 = substr($0, RLENGTH + 1)
+ return Token
+ } else if (match($0, /^(0[xX][0-9a-fA-F]+|[1-9][0-9]*)/)) {
+ Token = substr($0, 1, RLENGTH)
+ $0 = substr($0, RLENGTH + 1)
+ return Token
+ } else if (/./) {
+ Token = substr($0, 1, 1)
+ $0 = substr($0, 2)
+ return Token
+ }
+ } while (/./ || getline > 0)
+ Token = ""
+ return Token
+}
+
+function expect(v) {
+ if (!v)
+ fatal("broken expectations at `" Token "' before `" $0 "'")
+ return v
+}
+
+function accept(what) {
+ if (Token != what)
+ return 0
+ nexttoken()
+ return 1
+}
+
+function identifier( v) {
+ if (Token !~ /^[[:alpha:]]/)
+ return 0
+ v = Token
+ nexttoken()
+ return v
+}
+
+function number( v) {
+ if (Token !~ /^[0-9]/)
+ return 0
+ v = Token
+ nexttoken()
+ return v
+}
+
+function readnumber( ident) {
+ ident = identifier()
+ if (!ident)
+ return expect(number())
+ if (!(ident in Consts))
+ fatal("unknown constant: " ident)
+ return Consts[ident]
+}
+
+function defconst( ident, num) {
+ if (!accept("const"))
+ return 0
+
+ ident = expect(identifier())
+ expect(accept("="))
+ num = readnumber()
+ if (ident in Consts)
+ fatal("constant redefined: " ident)
+
+ Consts[ident] = num
+ codegen_constant(ident, num)
+ return 1
+}
+
+function readtype( ident) {
+ ident = deftype()
+ if (ident)
+ return ident
+
+ ident = identifier()
+ if (!ident)
+ return 0
+
+ if (!(ident in Types))
+ fatal("unknown type: " ident)
+ return ident
+}
+
+function defenum( name, ident, value, cg) {
+ delete cg[0]
+
+ name = expect(identifier())
+ expect(accept("{"))
+ while (!accept("}")) {
+ ident = expect(identifier())
+ value = value + 1
+ if (accept("="))
+ value = readnumber()
+ if (!value)
+ fatal("enumeration values cannot be zero")
+ if (value < -128 || value > 127)
+ fatal("enumeration value out of range")
+ expect(accept(","))
+ append(EnumValues, name, SUBSEP ident)
+ if (EnumValues[name, ident]++)
+ fatal("duplicate enum value: " ident)
+ codegen_enum_value(name, ident, value, cg)
+ }
+
+ Types[name] = "enum"
+ codegen_enum(name, cg)
+ return name
+}
+
+function readfield(out, nonvoid) {
+ nonvoid = !accept("void")
+ if (nonvoid) {
+ out["type"] = expect(readtype())
+ out["name"] = expect(identifier())
+ # TODO: Consider supporting XDR's VLA length limits here.
+ # TODO: Consider supporting XDR's fixed-length syntax for string limits.
+ out["isarray"] = accept("<") && expect(accept(">"))
+ }
+ expect(accept(";"))
+ return nonvoid
+}
+
+function defstruct( name, d, cg) {
+ delete d[0]
+ delete cg[0]
+
+ name = expect(identifier())
+ expect(accept("{"))
+ while (!accept("}")) {
+ if (readfield(d))
+ codegen_struct_field(d, cg)
+ }
+
+ Types[name] = "struct"
+ codegen_struct(name, cg)
+ return name
+}
+
+function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
+ delete cg[0]
+ delete scg[0]
+ delete d[0]
+
+ name = expect(identifier())
+ expect(accept("switch"))
+ expect(accept("("))
+ tag["type"] = tagtype = expect(readtype())
+ tag["name"] = expect(identifier())
+ expect(accept(")"))
+
+ if (Types[tagtype] != "enum")
+ fatal("not an enum type: " tagtype)
+ codegen_union_tag(tag, cg)
+
+ split(EnumValues[tagtype], a, SUBSEP)
+ for (i in a)
+ unseen[a[i]]++
+
+ expect(accept("{"))
+ while (!accept("}")) {
+ if (accept("case")) {
+ if (tagvalue)
+ codegen_union_struct(name, tagvalue, cg, scg)
+
+ tagvalue = expect(identifier())
+ expect(accept(":"))
+ if (!unseen[tagvalue]--)
+ fatal("no such value or duplicate case: " tagtype "." tagvalue)
+ codegen_struct_tag(tag, scg)
+ } else if (tagvalue) {
+ if (readfield(d))
+ codegen_struct_field(d, scg)
+ } else {
+ fatal("union fields must fall under a case")
+ }
+ }
+ if (tagvalue)
+ codegen_union_struct(name, tagvalue, cg, scg)
+
+ # What remains non-zero in unseen[2..] is simply not recognized/allowed.
+ Types[name] = "union"
+ codegen_union(name, cg)
+ return name
+}
+
+function deftype() {
+ if (accept("enum"))
+ return defenum()
+ if (accept("struct"))
+ return defstruct()
+ if (accept("union"))
+ return defunion()
+ return 0
+}
+
+BEGIN {
+ PrefixLower = "relay_"
+ PrefixUpper = "RELAY_"
+ PrefixCamel = "Relay"
+
+ print "// Generated by xC-gen-proto.awk. DO NOT MODIFY."
+ codegen_begin()
+
+ nexttoken()
+ while (Token != "") {
+ expect(defconst() || deftype())
+ expect(accept(";"))
+ }
+}
diff --git a/xC-proto b/xC-proto
new file mode 100644
index 0000000..3057404
--- /dev/null
+++ b/xC-proto
@@ -0,0 +1,206 @@
+// Backwards-compatible protocol version.
+const VERSION = 1;
+
+// From the frontend to the relay.
+struct CommandMessage {
+ // The command sequence number will be repeated in responses
+ // in the respective fields.
+ u32 command_seq;
+ union CommandData switch (enum Command {
+ HELLO,
+ ACTIVE,
+ BUFFER_ACTIVATE,
+ BUFFER_INPUT,
+ BUFFER_TOGGLE_UNIMPORTANT,
+ PING_RESPONSE,
+ PING,
+ BUFFER_COMPLETE,
+ BUFFER_LOG,
+ } command) {
+ // If the version check succeeds, the client will receive
+ // an initial stream of SERVER_UPDATE, BUFFER_UPDATE, BUFFER_STATS,
+ // BUFFER_LINE, and finally a BUFFER_ACTIVATE message.
+ case HELLO:
+ u32 version;
+ case ACTIVE:
+ void;
+ case BUFFER_ACTIVATE:
+ string buffer_name;
+ case BUFFER_INPUT:
+ string buffer_name;
+ string text;
+ // XXX: Perhaps this should rather be handled through a /buffer command.
+ case BUFFER_TOGGLE_UNIMPORTANT:
+ string buffer_name;
+ case PING_RESPONSE:
+ u32 event_seq;
+
+ // Only these commands may produce Event.RESPONSE, as below,
+ // but any command may produce an error.
+ case PING:
+ void;
+ case BUFFER_COMPLETE:
+ string buffer_name;
+ string text;
+ u32 position;
+ case BUFFER_LOG:
+ string buffer_name;
+ } data;
+};
+
+// From the relay to the frontend.
+struct EventMessage {
+ u32 event_seq;
+ union EventData switch (enum Event {
+ PING,
+ BUFFER_LINE,
+ BUFFER_UPDATE,
+ BUFFER_STATS,
+ BUFFER_RENAME,
+ BUFFER_REMOVE,
+ BUFFER_ACTIVATE,
+ BUFFER_CLEAR,
+ SERVER_UPDATE,
+ SERVER_RENAME,
+ SERVER_REMOVE,
+ ERROR,
+ RESPONSE,
+ } event) {
+ case PING:
+ void;
+
+ case BUFFER_LINE:
+ string buffer_name;
+ // Whether the line should also be displayed in the active buffer.
+ bool leak_to_active;
+ bool is_unimportant;
+ bool is_highlight;
+ enum Rendition {
+ BARE,
+ INDENT,
+ STATUS,
+ ERROR,
+ JOIN,
+ PART,
+ ACTION,
+ } rendition;
+ // Unix timestamp in milliseconds.
+ u64 when;
+ // Broken-up text, with in-band formatting.
+ union ItemData switch (enum Item {
+ TEXT,
+ RESET,
+ FG_COLOR,
+ BG_COLOR,
+ FLIP_BOLD,
+ FLIP_ITALIC,
+ FLIP_UNDERLINE,
+ FLIP_INVERSE,
+ FLIP_CROSSED_OUT,
+ FLIP_MONOSPACE,
+ } kind) {
+ case TEXT:
+ string text;
+ case RESET:
+ void;
+ case FG_COLOR:
+ i16 color;
+ case BG_COLOR:
+ i16 color;
+ case FLIP_BOLD:
+ case FLIP_ITALIC:
+ case FLIP_UNDERLINE:
+ case FLIP_INVERSE:
+ case FLIP_CROSSED_OUT:
+ case FLIP_MONOSPACE:
+ void;
+ } items<>;
+ case BUFFER_UPDATE:
+ string buffer_name;
+ bool hide_unimportant;
+ union BufferContext switch (enum BufferKind {
+ GLOBAL,
+ SERVER,
+ CHANNEL,
+ PRIVATE_MESSAGE,
+ } kind) {
+ case GLOBAL:
+ void;
+ case SERVER:
+ string server_name;
+ case CHANNEL:
+ string server_name;
+ ItemData topic<>;
+ // This includes parameters, separated by spaces.
+ string modes;
+ case PRIVATE_MESSAGE:
+ string server_name;
+ } context;
+ case BUFFER_STATS:
+ string buffer_name;
+ // These are cumulative, even for lines flushed out from buffers.
+ // Updates to these values aren't broadcasted, thus handle:
+ // - BUFFER_LINE by bumping/setting them as appropriate,
+ // - BUFFER_ACTIVATE by clearing them for the previous buffer
+ // (this way, they can be used to mark unread messages).
+ u32 new_messages;
+ u32 new_unimportant_messages;
+ bool highlighted;
+ case BUFFER_RENAME:
+ string buffer_name;
+ string new;
+ case BUFFER_REMOVE:
+ string buffer_name;
+ case BUFFER_ACTIVATE:
+ string buffer_name;
+ case BUFFER_CLEAR:
+ string buffer_name;
+
+ case SERVER_UPDATE:
+ string server_name;
+ union ServerData switch (enum ServerState {
+ DISCONNECTED,
+ CONNECTING,
+ CONNECTED,
+ REGISTERED,
+ DISCONNECTING,
+ } state) {
+ case DISCONNECTED:
+ case CONNECTING:
+ case CONNECTED:
+ void;
+ case REGISTERED:
+ string user;
+ string user_modes;
+ // Theoretically, we could also send user information in this state,
+ // but we'd have to duplicate both fields.
+ case DISCONNECTING:
+ void;
+ } data;
+ case SERVER_RENAME:
+ // Buffers aren't sent updates for in this circumstance,
+ // as that wouldn't be sufficiently atomic anyway.
+ string server_name;
+ string new;
+ case SERVER_REMOVE:
+ string server_name;
+
+ // Restriction: command_seq strictly follows the sequence received
+ // by the relay, across both of these replies.
+ case ERROR:
+ u32 command_seq;
+ string error;
+ case RESPONSE:
+ u32 command_seq;
+ union ResponseData switch (Command command) {
+ case PING:
+ void;
+ case BUFFER_COMPLETE:
+ u32 start;
+ string completions<>;
+ case BUFFER_LOG:
+ // UTF-8, but not guaranteed.
+ u8 log<>;
+ } data;
+ } data;
+};
diff --git a/xC.adoc b/xC.adoc
new file mode 100644
index 0000000..e2ef827
--- /dev/null
+++ b/xC.adoc
@@ -0,0 +1,127 @@
+xC(1)
+=====
+:doctype: manpage
+:manmanual: xK Manual
+:mansource: xK {release-version}
+
+Name
+----
+xC - terminal-based IRC client
+
+Synopsis
+--------
+*xC* [_OPTION_]...
+
+Description
+-----------
+*xC* is a scriptable IRC client for the command line. On the first run it will
+welcome you with an introductory message. Should you ever get lost, use the
+*/help* command to obtain more information on commands or options.
+
+Options
+-------
+*-f*, *--format*::
+ Format IRC text from the standard input, converting colour sequences and
+ other formatting marks to ANSI codes retrieved from the *terminfo*(5)
+ database:
++
+....
+printf '\x02bold\x02\n' | xC -f
+....
++
+This feature may be used to preview server MOTD files.
+
+*-h*, *--help*::
+ Display a help message and exit.
+
+*-V*, *--version*::
+ Output version information and exit.
+
+Key bindings
+------------
+Most key bindings are inherited from the frontend in use, which is either GNU
+Readline or BSD editline. A few of them, however, are special to the IRC client
+or assume a different function. This is a list of all local overrides and
+their respective function names:
+
+*M-p*::
+ Go up in history for this buffer (normally mapped to *C-p*).
+*M-n*::
+ Go down in history for this buffer (normally mapped to *C-n*).
+*C-p*, *F5*: *previous-buffer*::
+ Switch to the previous buffer in order.
+*C-n*, *F6*: *next-buffer*::
+ Switch to the next buffer in order.
+*M-TAB*: *switch-buffer*::
+ Switch to the last buffer, i.e., the one you were in before.
+*M-0*, *M-1*, ..., *M-9*: *goto-buffer*::
+ Go to the N-th buffer (normally sets a repeat counter).
+ Since there is no buffer number zero, *M-0* goes to the tenth one.
+*M-!*: *goto-highlight*::
+ Go to the first following buffer with an unseen highlight.
+*M-a*: *goto-activity*::
+ Go to the first following buffer with unseen activity.
+*PageUp*: *display-backlog*::
+ Show the in-memory backlog for this buffer using *general.pager*,
+ which is almost certainly the *less*(1) program.
+*M-h*: *display-full-log*::
+ Show the log file for this buffer using *general.pager*.
+*M-H*: *toggle-unimportant*::
+ Hide all join, part and quit messages, as well as all channel mode changes
+ that only relate to user channel modes. Intended to reduce noise in
+ channels with lots of people.
+*M-e*: *edit-input*::
+ Run an editor on the command line, making it easy to edit multiline
+ messages. Remember to save the file before exit.
+*M-m*: *insert-attribute*::
+ The next key will be interpreted as a formatting mark to insert:
+ *c* for colours (optionally followed by numbers for the foreground
+ and background), *i* for italics, *b* for bold text, *u* for underlined,
+ *s* for struck-through, *m* for monospace, *v* for inverse text,
+ and *o* resets all formatting.
+*C-l*: *redraw-screen*::
+ Should there be any issues with the display, this will clear the terminal
+ screen and redraw all information.
+
+Additionally, *C-w* and *C-u* in editline behave the same as they would in
+Readline or the "vi" command mode, even though the "emacs" mode is enabled
+by default.
+
+Bindings can be customized in your _.inputrc_ or _.editrc_ file. Both libraries
+support conditional execution based on the program name. Beware that it is easy
+to make breaking changes.
+
+Environment
+-----------
+*VISUAL*, *EDITOR*::
+ The editor program to be launched by the *edit-input* function.
+ If neither variable is set, it defaults to *vi*(1).
+
+Files
+-----
+*xC* follows the XDG Base Directory Specification.
+
+_~/.config/xC/xC.conf_::
+ The program's configuration file. Preferrably use internal facilities, such
+ as the */set* command, to make changes in it.
+
+_~/.local/share/xC/logs/_::
+ When enabled by *general.logging*, log files are stored here.
+
+_~/.local/share/xC/plugins/_::
+_/usr/local/share/xC/plugins/_::
+_/usr/share/xC/plugins/_::
+ Plugins are searched for in these directories, in order.
+
+Bugs
+----
+The editline (libedit) frontend may exhibit some unexpected behaviour.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/xK to report bugs, request features,
+or submit pull requests.
+
+See also
+--------
+*less*(1), *readline*(3) or *editline*(7)
diff --git a/xC.c b/xC.c
new file mode 100644
index 0000000..1b90964
--- /dev/null
+++ b/xC.c
@@ -0,0 +1,15971 @@
+/*
+ * xC.c: a terminal-based IRC client
+ *
+ * Copyright (c) 2015 - 2022, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ */
+
+// A table of all attributes we use for output
+#define ATTR_TABLE(XX) \
+ XX( PROMPT, prompt, Terminal attrs for the prompt ) \
+ XX( DATE_CHANGE, date_change, Terminal attrs for date change ) \
+ XX( READ_MARKER, read_marker, Terminal attrs for the read marker ) \
+ XX( WARNING, warning, Terminal attrs for warnings ) \
+ XX( ERROR, error, Terminal attrs for errors ) \
+ XX( EXTERNAL, external, Terminal attrs for external lines ) \
+ XX( TIMESTAMP, timestamp, Terminal attrs for timestamps ) \
+ XX( HIGHLIGHT, highlight, Terminal attrs for highlights ) \
+ XX( ACTION, action, Terminal attrs for user actions ) \
+ XX( USERHOST, userhost, Terminal attrs for user@host ) \
+ XX( JOIN, join, Terminal attrs for joins ) \
+ XX( PART, part, Terminal attrs for parts )
+
+enum
+{
+ ATTR_RESET,
+#define XX(x, y, z) ATTR_ ## x,
+ ATTR_TABLE (XX)
+#undef XX
+ ATTR_COUNT
+};
+
+// User data for logger functions to enable formatted logging
+#define print_fatal_data ((void *) ATTR_ERROR)
+#define print_error_data ((void *) ATTR_ERROR)
+#define print_warning_data ((void *) ATTR_WARNING)
+
+#include "config.h"
+#define PROGRAM_NAME "xC"
+
+// fmemopen
+#define _POSIX_C_SOURCE 200809L
+
+#include "common.c"
+#include "xD-replies.c"
+#include "xC-proto.c"
+
+#include <math.h>
+#include <langinfo.h>
+#include <locale.h>
+#include <pwd.h>
+#include <sys/utsname.h>
+#include <wchar.h>
+
+#include <termios.h>
+#include <sys/ioctl.h>
+
+#include <curses.h>
+#include <term.h>
+
+// Literally cancer
+#undef lines
+#undef columns
+
+#include <ffi.h>
+
+#ifdef HAVE_LUA
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+#endif // HAVE_LUA
+
+// --- Terminal information ----------------------------------------------------
+
+static struct
+{
+ bool initialized; ///< Terminal is available
+ bool stdout_is_tty; ///< `stdout' is a terminal
+ bool stderr_is_tty; ///< `stderr' is a terminal
+
+ struct termios termios; ///< Terminal attributes
+ char *color_set_fg[256]; ///< Codes to set the foreground colour
+ char *color_set_bg[256]; ///< Codes to set the background colour
+
+ int lines; ///< Number of lines
+ int columns; ///< Number of columns
+}
+g_terminal;
+
+static void
+update_screen_size (void)
+{
+#ifdef TIOCGWINSZ
+ if (!g_terminal.stdout_is_tty)
+ return;
+
+ struct winsize size;
+ if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
+ {
+ char *row = getenv ("LINES");
+ char *col = getenv ("COLUMNS");
+ unsigned long tmp;
+ g_terminal.lines =
+ (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row;
+ g_terminal.columns =
+ (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col;
+ }
+#endif // TIOCGWINSZ
+}
+
+static bool
+init_terminal (void)
+{
+ int tty_fd = -1;
+ if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
+ tty_fd = STDERR_FILENO;
+ if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
+ tty_fd = STDOUT_FILENO;
+
+ int err;
+ if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
+ return false;
+
+ // Make sure all terminal features used by us are supported
+ if (!set_a_foreground || !set_a_background
+ || !enter_bold_mode || !exit_attribute_mode
+ || tcgetattr (tty_fd, &g_terminal.termios))
+ {
+ del_curterm (cur_term);
+ return false;
+ }
+
+ // Make sure newlines are output correctly
+ g_terminal.termios.c_oflag |= ONLCR;
+ (void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios);
+
+ g_terminal.lines = tigetnum ("lines");
+ g_terminal.columns = tigetnum ("cols");
+ update_screen_size ();
+
+ int max = MIN (256, max_colors);
+ for (int i = 0; i < max; i++)
+ {
+ g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
+ i, 0, 0, 0, 0, 0, 0, 0, 0));
+ g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
+ i, 0, 0, 0, 0, 0, 0, 0, 0));
+ }
+ return g_terminal.initialized = true;
+}
+
+static void
+free_terminal (void)
+{
+ if (!g_terminal.initialized)
+ return;
+
+ for (int i = 0; i < 256; i++)
+ {
+ free (g_terminal.color_set_fg[i]);
+ free (g_terminal.color_set_bg[i]);
+ }
+ del_curterm (cur_term);
+}
+
+// --- User interface ----------------------------------------------------------
+
+// I'm not sure which one of these backends is worse: whether it's GNU Readline
+// or BSD Editline. They both have their own annoying problems. We use lots
+// of hacks to get the results we want and need.
+//
+// The abstraction is a necessary evil. It's still not 100%, though.
+
+/// Some arbitrary limit for the history
+#define HISTORY_LIMIT 10000
+
+/// Characters that separate words
+#define WORD_BREAKING_CHARS " \f\n\r\t\v"
+
+struct input
+{
+ struct input_vtable *vtable; ///< Virtual methods
+ void (*add_functions) (void *); ///< Define functions for binding
+ void *user_data; ///< User data for callbacks
+};
+
+typedef void *input_buffer_t; ///< Pointer alias for input buffers
+
+/// Named function that can be bound to a sequence of characters
+typedef bool (*input_fn) (int count, int key, void *user_data);
+
+// A little bit better than tons of forwarder functions in our case
+#define CALL(self, name) ((self)->vtable->name ((self)))
+#define CALL_(self, name, ...) ((self)->vtable->name ((self), __VA_ARGS__))
+
+struct input_vtable
+{
+ /// Start the interface under the given program name
+ void (*start) (void *input, const char *program_name);
+ /// Stop the interface
+ void (*stop) (void *input);
+ /// Prepare or unprepare terminal for our needs
+ void (*prepare) (void *input, bool enabled);
+ /// Destroy the object
+ void (*destroy) (void *input);
+
+ /// Hide the prompt if shown
+ void (*hide) (void *input);
+ /// Show the prompt if hidden
+ void (*show) (void *input);
+ /// Retrieve current prompt string
+ const char *(*get_prompt) (void *input);
+ /// Change the prompt string; takes ownership
+ void (*set_prompt) (void *input, char *prompt);
+ /// Ring the terminal bell
+ void (*ding) (void *input);
+
+ /// Create a new input buffer
+ input_buffer_t (*buffer_new) (void *input);
+ /// Destroy an input buffer
+ void (*buffer_destroy) (void *input, input_buffer_t buffer);
+ /// Switch to a different input buffer
+ void (*buffer_switch) (void *input, input_buffer_t buffer);
+
+ /// Register a function that can be bound to character sequences
+ void (*register_fn) (void *input,
+ const char *name, const char *help, input_fn fn, void *user_data);
+ /// Bind an arbitrary sequence of characters to the given named function
+ void (*bind) (void *input, const char *seq, const char *fn);
+ /// Bind Ctrl+key to the given named function
+ void (*bind_control) (void *input, char key, const char *fn);
+ /// Bind Alt+key to the given named function
+ void (*bind_meta) (void *input, char key, const char *fn);
+
+ /// Get the current line input and position within
+ char *(*get_line) (void *input, int *position);
+ /// Clear the current line input
+ void (*clear_line) (void *input);
+ /// Insert text at current position
+ bool (*insert) (void *input, const char *text);
+
+ /// Handle terminal resize
+ void (*on_tty_resized) (void *input);
+ /// Handle terminal input
+ void (*on_tty_readable) (void *input);
+};
+
+#define INPUT_VTABLE(XX) \
+ XX (start) XX (stop) XX (prepare) XX (destroy) \
+ XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \
+ XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \
+ XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta) \
+ XX (get_line) XX (clear_line) XX (insert) \
+ XX (on_tty_resized) XX (on_tty_readable)
+
+// ~~~ GNU Readline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#ifdef HAVE_READLINE
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE
+#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE
+
+struct input_rl_fn
+{
+ ffi_closure closure; ///< Closure
+
+ LIST_HEADER (struct input_rl_fn)
+ input_fn callback; ///< Real callback
+ void *user_data; ///< Real callback user data
+};
+
+struct input_rl_buffer
+{
+ HISTORY_STATE *history; ///< Saved history state
+ char *saved_line; ///< Saved line content
+ int saved_point; ///< Saved cursor position
+ int saved_mark; ///< Saved mark
+};
+
+struct input_rl
+{
+ struct input super; ///< Parent class
+
+ bool active; ///< Interface has been started
+ char *prompt; ///< The prompt we use
+ int prompt_shown; ///< Whether the prompt is shown now
+
+ char *saved_line; ///< Saved line content
+ int saved_point; ///< Saved cursor position
+ int saved_mark; ///< Saved mark
+
+ struct input_rl_fn *fns; ///< Named functions
+ struct input_rl_buffer *current; ///< Current input buffer
+};
+
+static void
+input_rl_ding (void *input)
+{
+ (void) input;
+ rl_ding ();
+}
+
+static const char *
+input_rl_get_prompt (void *input)
+{
+ struct input_rl *self = input;
+ return self->prompt;
+}
+
+static void
+input_rl_set_prompt (void *input, char *prompt)
+{
+ struct input_rl *self = input;
+ cstr_set (&self->prompt, prompt);
+
+ if (!self->active || self->prompt_shown <= 0)
+ return;
+
+ // First reset the prompt to work around a bug in readline
+ rl_set_prompt ("");
+ rl_redisplay ();
+
+ rl_set_prompt (self->prompt);
+ rl_redisplay ();
+}
+
+static void
+input_rl_clear_line (void *input)
+{
+ (void) input;
+ rl_replace_line ("", false);
+ rl_redisplay ();
+}
+
+static void
+input_rl__erase (struct input_rl *self)
+{
+ rl_set_prompt ("");
+ input_rl_clear_line (self);
+}
+
+static bool
+input_rl_insert (void *input, const char *s)
+{
+ struct input_rl *self = input;
+ rl_insert_text (s);
+ if (self->prompt_shown > 0)
+ rl_redisplay ();
+
+ // GNU Readline, contrary to Editline, doesn't care about validity
+ return true;
+}
+
+static char *
+input_rl_get_line (void *input, int *position)
+{
+ (void) input;
+ if (position) *position = rl_point;
+ return rl_copy_text (0, rl_end);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_rl_bind (void *input, const char *seq, const char *function_name)
+{
+ (void) input;
+ rl_bind_keyseq (seq, rl_named_function (function_name));
+}
+
+static void
+input_rl_bind_meta (void *input, char key, const char *function_name)
+{
+ // This one seems to actually work
+ char keyseq[] = { '\\', 'e', key, 0 };
+ input_rl_bind (input, keyseq, function_name);
+#if 0
+ // While this one only fucks up UTF-8
+ // Tested with urxvt and xterm, on Debian Jessie/Arch, default settings
+ // \M-<key> behaves exactly the same
+ rl_bind_key (META (key), rl_named_function (function_name));
+#endif
+}
+
+static void
+input_rl_bind_control (void *input, char key, const char *function_name)
+{
+ char keyseq[] = { '\\', 'C', '-', key, 0 };
+ input_rl_bind (input, keyseq, function_name);
+}
+
+static void
+input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
+{
+ (void) cif;
+
+ struct input_rl_fn *data = user_data;
+ if (!data->callback
+ (*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data))
+ rl_ding ();
+ *(int *) ret = 0;
+}
+
+static void
+input_rl_register_fn (void *input,
+ const char *name, const char *help, input_fn callback, void *user_data)
+{
+ struct input_rl *self = input;
+ (void) help;
+
+ void *bound_fn = NULL;
+ struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
+ hard_assert (data);
+
+ static ffi_cif cif;
+ static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint };
+ hard_assert (ffi_prep_cif
+ (&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK);
+
+ data->prev = data->next = NULL;
+ data->callback = callback;
+ data->user_data = user_data;
+ hard_assert (ffi_prep_closure_loc (&data->closure,
+ &cif, input_rl__forward, data, bound_fn) == FFI_OK);
+
+ rl_add_defun (name, (rl_command_func_t *) bound_fn, -1);
+ LIST_PREPEND (self->fns, data);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int app_readline_init (void);
+static void on_readline_input (char *line);
+static char **app_readline_completion (const char *text, int start, int end);
+
+static void
+input_rl_start (void *input, const char *program_name)
+{
+ struct input_rl *self = input;
+ using_history ();
+ // This can cause memory leaks, or maybe even a segfault. Funny, eh?
+ stifle_history (HISTORY_LIMIT);
+
+ const char *slash = strrchr (program_name, '/');
+ rl_readline_name = slash ? ++slash : program_name;
+ rl_startup_hook = app_readline_init;
+ rl_catch_sigwinch = false;
+ rl_change_environment = false;
+
+ rl_basic_word_break_characters = WORD_BREAKING_CHARS;
+ rl_completer_word_break_characters = NULL;
+ rl_attempted_completion_function = app_readline_completion;
+
+ // We shouldn't produce any duplicates that the library would help us
+ // autofilter, and we don't generally want alphabetic ordering at all
+ rl_sort_completion_matches = false;
+
+ hard_assert (self->prompt != NULL);
+ // The inputrc is read before any callbacks are called, so we need to
+ // register all functions that our user may want to map up front
+ self->super.add_functions (self->super.user_data);
+ rl_callback_handler_install (self->prompt, on_readline_input);
+
+ self->prompt_shown = 1;
+ self->active = true;
+}
+
+static void
+input_rl_stop (void *input)
+{
+ struct input_rl *self = input;
+ if (self->prompt_shown > 0)
+ input_rl__erase (self);
+
+ // This is okay as long as we're not called from within readline
+ rl_callback_handler_remove ();
+ self->active = false;
+ self->prompt_shown = false;
+}
+
+static void
+input_rl_prepare (void *input, bool enabled)
+{
+ (void) input;
+ if (enabled)
+ rl_prep_terminal (true);
+ else
+ rl_deprep_terminal ();
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The following part shows you why it's not a good idea to use
+// GNU Readline for this kind of software. Or for anything else, really.
+
+static void
+input_rl__save_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
+{
+ (void) self;
+
+ buffer->history = history_get_history_state ();
+ buffer->saved_line = rl_copy_text (0, rl_end);
+ buffer->saved_point = rl_point;
+ buffer->saved_mark = rl_mark;
+
+ rl_replace_line ("", true);
+ if (self->prompt_shown > 0)
+ rl_redisplay ();
+}
+
+static void
+input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
+{
+ if (buffer->history)
+ {
+ // history_get_history_state() just allocates a new HISTORY_STATE
+ // and fills it with its current internal data. We don't need that
+ // shell anymore after reviving it.
+ history_set_history_state (buffer->history);
+ free (buffer->history);
+ buffer->history = NULL;
+ }
+ else
+ {
+ // This should get us a clean history while keeping the flags.
+ // Note that we've either saved the previous history entries, or we've
+ // cleared them altogether, so there should be nothing to leak.
+ HISTORY_STATE *state = history_get_history_state ();
+ state->offset = state->length = state->size = 0;
+ state->entries = NULL;
+ history_set_history_state (state);
+ free (state);
+ }
+
+ if (buffer->saved_line)
+ {
+ rl_replace_line (buffer->saved_line, true);
+ rl_point = buffer->saved_point;
+ rl_mark = buffer->saved_mark;
+ cstr_set (&buffer->saved_line, NULL);
+
+ if (self->prompt_shown > 0)
+ rl_redisplay ();
+ }
+}
+
+static void
+input_rl_buffer_switch (void *input, input_buffer_t input_buffer)
+{
+ struct input_rl *self = input;
+ struct input_rl_buffer *buffer = input_buffer;
+ // There could possibly be occurences of the current undo list in some
+ // history entry. We either need to free the undo list, or move it
+ // somewhere else to load back later, as the buffer we're switching to
+ // has its own history state.
+ rl_free_undo_list ();
+
+ // Save this buffer's history so that it's independent for each buffer
+ if (self->current)
+ input_rl__save_buffer (self, self->current);
+ else
+ // Just throw it away; there should always be an active buffer however
+#if RL_READLINE_VERSION >= 0x0603
+ rl_clear_history ();
+#else // RL_READLINE_VERSION < 0x0603
+ // At least something... this may leak undo entries
+ clear_history ();
+#endif // RL_READLINE_VERSION < 0x0603
+
+ input_rl__restore_buffer (self, buffer);
+ self->current = buffer;
+}
+
+static void
+input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self)
+{
+ free (self->history);
+ free (self->saved_line);
+ free (self);
+}
+
+static void
+input_rl_buffer_destroy (void *input, input_buffer_t input_buffer)
+{
+ (void) input;
+ struct input_rl_buffer *buffer = input_buffer;
+
+ // rl_clear_history, being the only way I know of to get rid of the complete
+ // history including attached data, is a pretty recent addition. *sigh*
+#if RL_READLINE_VERSION >= 0x0603
+ if (buffer->history)
+ {
+ // See input_rl_buffer_switch() for why we need to do this BS
+ rl_free_undo_list ();
+
+ // This is probably the only way we can free the history fully
+ HISTORY_STATE *state = history_get_history_state ();
+
+ history_set_history_state (buffer->history);
+ rl_clear_history ();
+ // rl_clear_history just removes history entries,
+ // we have to reclaim memory for their actual container ourselves
+ free (buffer->history->entries);
+ free (buffer->history);
+ buffer->history = NULL;
+
+ history_set_history_state (state);
+ free (state);
+ }
+#endif // RL_READLINE_VERSION
+
+ input_rl__buffer_destroy_wo_history (buffer);
+}
+
+static input_buffer_t
+input_rl_buffer_new (void *input)
+{
+ (void) input;
+ struct input_rl_buffer *self = xcalloc (1, sizeof *self);
+ return self;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Since {save,restore}_buffer() store history, we can't use them here like we
+// do with libedit, because then buffer_destroy() can free memory that's still
+// being used by readline. This situation is bound to happen on quit.
+
+static void
+input_rl__save (struct input_rl *self)
+{
+ hard_assert (!self->saved_line);
+
+ self->saved_point = rl_point;
+ self->saved_mark = rl_mark;
+ self->saved_line = rl_copy_text (0, rl_end);
+}
+
+static void
+input_rl__restore (struct input_rl *self)
+{
+ hard_assert (self->saved_line);
+
+ rl_set_prompt (self->prompt);
+ rl_replace_line (self->saved_line, false);
+ rl_point = self->saved_point;
+ rl_mark = self->saved_mark;
+ cstr_set (&self->saved_line, NULL);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_rl_hide (void *input)
+{
+ struct input_rl *self = input;
+ if (!self->active || self->prompt_shown-- < 1)
+ return;
+
+ input_rl__save (self);
+ input_rl__erase (self);
+}
+
+static void
+input_rl_show (void *input)
+{
+ struct input_rl *self = input;
+ if (!self->active || ++self->prompt_shown < 1)
+ return;
+
+ input_rl__restore (self);
+ rl_redisplay ();
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_rl_on_tty_resized (void *input)
+{
+ (void) input;
+ // This fucks up big time on terminals with automatic wrapping such as
+ // rxvt-unicode or newer VTE when the current line overflows, however we
+ // can't do much about that
+ rl_resize_terminal ();
+}
+
+static void
+input_rl_on_tty_readable (void *input)
+{
+ (void) input;
+ rl_callback_read_char ();
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_rl_destroy (void *input)
+{
+ struct input_rl *self = input;
+ free (self->saved_line);
+ LIST_FOR_EACH (struct input_rl_fn, iter, self->fns)
+ ffi_closure_free (iter);
+ free (self->prompt);
+ free (self);
+}
+
+#define XX(a) .a = input_rl_ ## a,
+static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
+#undef XX
+
+static struct input *
+input_rl_new (void)
+{
+ struct input_rl *self = xcalloc (1, sizeof *self);
+ self->super.vtable = &input_rl_vtable;
+ return &self->super;
+}
+
+#define input_new input_rl_new
+#endif // HAVE_READLINE
+
+// ~~~ BSD Editline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#ifdef HAVE_EDITLINE
+
+#include <histedit.h>
+
+#define INPUT_START_IGNORE '\x01'
+#define INPUT_END_IGNORE '\x01'
+
+struct input_el_fn
+{
+ ffi_closure closure; ///< Closure
+
+ LIST_HEADER (struct input_el_fn)
+ input_fn callback; ///< Real callback
+ void *user_data; ///< Real callback user data
+
+ wchar_t *name; ///< Function name
+ wchar_t *help; ///< Function help
+};
+
+struct input_el_buffer
+{
+ HistoryW *history; ///< The history object
+ wchar_t *saved_line; ///< Saved line content
+ int saved_len; ///< Length of the saved line
+ int saved_point; ///< Saved cursor position
+};
+
+struct input_el
+{
+ struct input super; ///< Parent class
+ EditLine *editline; ///< The EditLine object
+
+ bool active; ///< Are we a thing?
+ char *prompt; ///< The prompt we use
+ int prompt_shown; ///< Whether the prompt is shown now
+
+ struct input_el_fn *fns; ///< Named functions
+ struct input_el_buffer *current; ///< Current input buffer
+};
+
+static void app_editline_init (struct input_el *self);
+
+static void
+input_el__redisplay (void *input)
+{
+ // See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT
+ // so we bind redisplay somewhere else in app_editline_init()
+ struct input_el *self = input;
+ wchar_t x[] = { L'q' & 31, 0 };
+ el_wpush (self->editline, x);
+
+ // We have to do this or it gets stuck and nothing is done
+ int dummy_count = 0;
+ (void) el_wgets (self->editline, &dummy_count);
+}
+
+static char *
+input_el__make_prompt (EditLine *editline)
+{
+ struct input_el *self = NULL;
+ el_get (editline, EL_CLIENTDATA, &self);
+ if (!self->prompt)
+ return "";
+ return self->prompt;
+}
+
+static char *
+input_el__make_empty_prompt (EditLine *editline)
+{
+ (void) editline;
+ return "";
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_ding (void *input)
+{
+ // XXX: this isn't probably very portable;
+ // we could use "bell" from terminfo but that creates a dependency
+ (void) input;
+ write (STDOUT_FILENO, "\a", 1);
+}
+
+static const char *
+input_el_get_prompt (void *input)
+{
+ struct input_el *self = input;
+ return self->prompt;
+}
+
+static void
+input_el_set_prompt (void *input, char *prompt)
+{
+ struct input_el *self = input;
+ cstr_set (&self->prompt, prompt);
+
+ if (self->prompt_shown > 0)
+ input_el__redisplay (self);
+}
+
+static void
+input_el_clear_line (void *input)
+{
+ struct input_el *self = input;
+ const LineInfoW *info = el_wline (self->editline);
+ int len = info->lastchar - info->buffer;
+ int point = info->cursor - info->buffer;
+ el_cursor (self->editline, len - point);
+ el_wdeletestr (self->editline, len);
+ input_el__redisplay (self);
+}
+
+static void
+input_el__erase (struct input_el *self)
+{
+ el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
+ input_el_clear_line (self);
+}
+
+static bool
+input_el_insert (void *input, const char *s)
+{
+ struct input_el *self = input;
+ bool success = !*s || !el_insertstr (self->editline, s);
+ if (self->prompt_shown > 0)
+ input_el__redisplay (self);
+ return success;
+}
+
+static char *
+input_el_get_line (void *input, int *position)
+{
+ struct input_el *self = input;
+ const LineInfo *info = el_line (self->editline);
+ int point = info->cursor - info->buffer;
+ if (position) *position = point;
+ return xstrndup (info->buffer, info->lastchar - info->buffer);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_bind (void *input, const char *seq, const char *function_name)
+{
+ struct input_el *self = input;
+ el_set (self->editline, EL_BIND, seq, function_name, NULL);
+}
+
+static void
+input_el_bind_meta (void *input, char key, const char *function_name)
+{
+ char keyseq[] = { 'M', '-', key, 0 };
+ input_el_bind (input, keyseq, function_name);
+}
+
+static void
+input_el_bind_control (void *input, char key, const char *function_name)
+{
+ char keyseq[] = { '^', key, 0 };
+ input_el_bind (input, keyseq, function_name);
+}
+
+static void
+input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
+{
+ (void) cif;
+
+ struct input_el_fn *data = user_data;
+ *(unsigned char *) ret = data->callback
+ (1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
+}
+
+static wchar_t *
+ascii_to_wide (const char *ascii)
+{
+ size_t len = strlen (ascii) + 1;
+ wchar_t *wide = xcalloc (sizeof *wide, len);
+ while (len--)
+ hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
+ return wide;
+}
+
+static void
+input_el_register_fn (void *input,
+ const char *name, const char *help, input_fn callback, void *user_data)
+{
+ void *bound_fn = NULL;
+ struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
+ hard_assert (data);
+
+ static ffi_cif cif;
+ static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
+ hard_assert (ffi_prep_cif
+ (&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
+
+ data->user_data = user_data;
+ data->callback = callback;
+ data->name = ascii_to_wide (name);
+ data->help = ascii_to_wide (help);
+ hard_assert (ffi_prep_closure_loc (&data->closure,
+ &cif, input_el__forward, data, bound_fn) == FFI_OK);
+
+ struct input_el *self = input;
+ el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
+ LIST_PREPEND (self->fns, data);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_start (void *input, const char *program_name)
+{
+ struct input_el *self = input;
+ self->editline = el_init (program_name, stdin, stdout, stderr);
+ el_set (self->editline, EL_CLIENTDATA, self);
+ el_set (self->editline, EL_PROMPT_ESC,
+ input_el__make_prompt, INPUT_START_IGNORE);
+ el_set (self->editline, EL_SIGNAL, false);
+ el_set (self->editline, EL_UNBUFFERED, true);
+ el_set (self->editline, EL_EDITOR, "emacs");
+
+ app_editline_init (self);
+
+ self->prompt_shown = 1;
+ self->active = true;
+}
+
+static void
+input_el_stop (void *input)
+{
+ struct input_el *self = input;
+ if (self->prompt_shown > 0)
+ input_el__erase (self);
+
+ el_end (self->editline);
+ self->editline = NULL;
+ self->active = false;
+ self->prompt_shown = false;
+}
+
+static void
+input_el_prepare (void *input, bool enabled)
+{
+ struct input_el *self = input;
+ el_set (self->editline, EL_PREP_TERM, enabled);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
+{
+ const LineInfoW *info = el_wline (self->editline);
+ int len = info->lastchar - info->buffer;
+ int point = info->cursor - info->buffer;
+
+ wchar_t *line = calloc (sizeof *info->buffer, len + 1);
+ memcpy (line, info->buffer, sizeof *info->buffer * len);
+ el_cursor (self->editline, len - point);
+ el_wdeletestr (self->editline, len);
+
+ buffer->saved_line = line;
+ buffer->saved_point = point;
+ buffer->saved_len = len;
+}
+
+static void
+input_el__save (struct input_el *self)
+{
+ if (self->current)
+ input_el__save_buffer (self, self->current);
+}
+
+static void
+input_el__restore_buffer (struct input_el *self, struct input_el_buffer *buffer)
+{
+ if (buffer->saved_line)
+ {
+ el_winsertstr (self->editline, buffer->saved_line);
+ el_cursor (self->editline,
+ -(buffer->saved_len - buffer->saved_point));
+ free (buffer->saved_line);
+ buffer->saved_line = NULL;
+ }
+}
+
+static void
+input_el__restore (struct input_el *self)
+{
+ if (self->current)
+ input_el__restore_buffer (self, self->current);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Editline keeping its own history position (look for "eventno" there).
+// This is the only sane way of resetting it.
+static void
+input_el__start_over (struct input_el *self)
+{
+ wchar_t x[] = { L'c' & 31, 0 };
+ el_wpush (self->editline, x);
+
+ int dummy_count = 0;
+ (void) el_wgets (self->editline, &dummy_count);
+}
+
+static void
+input_el_buffer_switch (void *input, input_buffer_t input_buffer)
+{
+ struct input_el *self = input;
+ struct input_el_buffer *buffer = input_buffer;
+ if (!self->active)
+ return;
+
+ if (self->current)
+ input_el__save_buffer (self, self->current);
+
+ self->current = buffer;
+ el_wset (self->editline, EL_HIST, history, buffer->history);
+ input_el__start_over (self);
+ input_el__restore_buffer (self, buffer);
+}
+
+static void
+input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
+{
+ struct input_el *self = input;
+ struct input_el_buffer *buffer = input_buffer;
+ if (self->active && self->current == buffer)
+ {
+ el_wset (self->editline, EL_HIST, history, NULL);
+ self->current = NULL;
+ }
+
+ history_wend (buffer->history);
+ free (buffer->saved_line);
+ free (buffer);
+}
+
+static input_buffer_t
+input_el_buffer_new (void *input)
+{
+ (void) input;
+ struct input_el_buffer *self = xcalloc (1, sizeof *self);
+ self->history = history_winit ();
+
+ HistEventW ev;
+ history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
+ return self;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_hide (void *input)
+{
+ struct input_el *self = input;
+ if (!self->active || self->prompt_shown-- < 1)
+ return;
+
+ input_el__save (self);
+ input_el__erase (self);
+}
+
+static void
+input_el_show (void *input)
+{
+ struct input_el *self = input;
+ if (!self->active || ++self->prompt_shown < 1)
+ return;
+
+ input_el__restore (self);
+ el_set (self->editline,
+ EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE);
+ input_el__redisplay (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_on_tty_resized (void *input)
+{
+ struct input_el *self = input;
+ el_resize (self->editline);
+}
+
+static void
+input_el_on_tty_readable (void *input)
+{
+ // We bind the return key to process it how we need to
+ struct input_el *self = input;
+
+ // el_gets() with EL_UNBUFFERED doesn't work with UTF-8,
+ // we must use the wide-character interface
+ int count = 0;
+ const wchar_t *buf = el_wgets (self->editline, &count);
+ if (!buf || count-- <= 0)
+ return;
+
+ if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in el_wgets() */)
+ {
+ el_deletestr (self->editline, 1);
+ input_el__redisplay (self);
+ input_el_ding (self);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+input_el_destroy (void *input)
+{
+ struct input_el *self = input;
+ LIST_FOR_EACH (struct input_el_fn, iter, self->fns)
+ {
+ free (iter->name);
+ free (iter->help);
+ ffi_closure_free (iter);
+ }
+ free (self->prompt);
+ free (self);
+}
+
+#define XX(a) .a = input_el_ ## a,
+static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) };
+#undef XX
+
+static struct input *
+input_el_new (void)
+{
+ struct input_el *self = xcalloc (1, sizeof *self);
+ self->super.vtable = &input_el_vtable;
+ return &self->super;
+}
+
+#define input_new input_el_new
+#endif // HAVE_EDITLINE
+
+// --- Application data --------------------------------------------------------
+
+// All text stored in our data structures is encoded in UTF-8. Or at least
+// should be--our only ways of retrieving strings are: via the command line
+// (converted from locale, no room for errors), via the configuration file
+// (restrictive ASCII grammar for bare words and an internal check for strings),
+// and via plugins (meticulously validated).
+//
+// The only exception is IRC identifiers.
+
+// ~~~ Scripting support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+// We need a few reference countable objects with support for both strong
+// and weak references (mainly used for scripted plugins).
+//
+// Beware that if you don't own the object, you will most probably want
+// to keep the weak reference link so that you can get rid of it later.
+// Also note that you have to make sure the user_data don't leak resources.
+//
+// Having a callback is more versatile than just nulling out a pointer.
+
+/// Callback just before a reference counted object is destroyed
+typedef void (*destroy_cb_fn) (void *object, void *user_data);
+
+struct weak_ref_link
+{
+ LIST_HEADER (struct weak_ref_link)
+
+ destroy_cb_fn on_destroy; ///< Called when object is destroyed
+ void *user_data; ///< User data
+};
+
+static struct weak_ref_link *
+weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data)
+{
+ struct weak_ref_link *link = xcalloc (1, sizeof *link);
+ link->on_destroy = cb;
+ link->user_data = user_data;
+ LIST_PREPEND (*list, link);
+ return link;
+}
+
+static void
+weak_unref (struct weak_ref_link **list, struct weak_ref_link **link)
+{
+ if (*link)
+ LIST_UNLINK (*list, *link);
+ free (*link);
+ *link = NULL;
+}
+
+#define REF_COUNTABLE_HEADER \
+ size_t ref_count; /**< Reference count */ \
+ struct weak_ref_link *weak_refs; /**< To remove any weak references */
+
+#define REF_COUNTABLE_METHODS(name) \
+ static struct name * \
+ name ## _ref (struct name *self) \
+ { \
+ self->ref_count++; \
+ return self; \
+ } \
+ \
+ static void \
+ name ## _unref (struct name *self) \
+ { \
+ if (--self->ref_count) \
+ return; \
+ LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \
+ { \
+ iter->on_destroy (self, iter->user_data); \
+ free (iter); \
+ } \
+ name ## _destroy (self); \
+ } \
+ \
+ static struct weak_ref_link * \
+ name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \
+ { return weak_ref (&self->weak_refs, cb, user_data); } \
+ \
+ static void \
+ name ## _weak_unref (struct name *self, struct weak_ref_link **link) \
+ { weak_unref (&self->weak_refs, link); }
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Simple introspection framework to simplify exporting stuff to Lua, since
+// there is a lot of it. While not fully automated, at least we have control
+// over which fields are exported.
+
+enum ispect_type
+{
+ ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING,
+
+ ISPECT_STR, ///< "struct str"
+ ISPECT_STR_MAP, ///< "struct str_map"
+ ISPECT_REF ///< Weakly referenced object
+};
+
+struct ispect_field
+{
+ const char *name; ///< Name of the field
+ size_t offset; ///< Offset in the structure
+ enum ispect_type type; ///< Type of the field
+
+ enum ispect_type subtype; ///< STR_MAP subtype
+ struct ispect_field *fields; ///< REF target fields
+ bool is_list; ///< REF target is a list
+};
+
+#define ISPECT(object, field, type) \
+ { #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false },
+#define ISPECT_REF(object, field, is_list, ref_type) \
+ { #field, offsetof (struct object, field), ISPECT_REF, 0, \
+ g_##ref_type##_ispect, is_list },
+#define ISPECT_MAP(object, field, subtype) \
+ { #field, offsetof (struct object, field), ISPECT_STR_MAP, \
+ ISPECT_##subtype, NULL, false },
+#define ISPECT_MAP_REF(object, field, is_list, ref_type) \
+ { #field, offsetof (struct object, field), ISPECT_STR_MAP, \
+ ISPECT_REF, g_##ref_type##_ispect, is_list },
+
+// ~~~ Chat ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+struct user_channel
+{
+ LIST_HEADER (struct user_channel)
+
+ struct channel *channel; ///< Reference to channel
+};
+
+static struct user_channel *
+user_channel_new (struct channel *channel)
+{
+ struct user_channel *self = xcalloc (1, sizeof *self);
+ self->channel = channel;
+ return self;
+}
+
+static void
+user_channel_destroy (struct user_channel *self)
+{
+ // The "channel" reference is weak and this object should get
+ // destroyed whenever the user stops being in the channel.
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// We keep references to user information in channels and buffers,
+// and weak references in the name lookup table.
+
+struct user
+{
+ REF_COUNTABLE_HEADER
+
+ char *nickname; ///< Literal nickname
+ bool away; ///< User is away
+
+ struct user_channel *channels; ///< Channels the user is on (with us)
+};
+
+static struct ispect_field g_user_ispect[] =
+{
+ ISPECT( user, nickname, STRING )
+ ISPECT( user, away, BOOL )
+ {}
+};
+
+static struct user *
+user_new (char *nickname)
+{
+ struct user *self = xcalloc (1, sizeof *self);
+ self->ref_count = 1;
+ self->nickname = nickname;
+ return self;
+}
+
+static void
+user_destroy (struct user *self)
+{
+ free (self->nickname);
+ LIST_FOR_EACH (struct user_channel, iter, self->channels)
+ user_channel_destroy (iter);
+ free (self);
+}
+
+REF_COUNTABLE_METHODS (user)
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct channel_user
+{
+ LIST_HEADER (struct channel_user)
+
+ struct user *user; ///< Reference to user
+ char *prefixes; ///< Ordered @+... characters
+};
+
+static struct channel_user *
+channel_user_new (struct user *user, const char *prefixes)
+{
+ struct channel_user *self = xcalloc (1, sizeof *self);
+ self->user = user;
+ self->prefixes = xstrdup (prefixes);
+ return self;
+}
+
+static void
+channel_user_destroy (struct channel_user *self)
+{
+ user_unref (self->user);
+ free (self->prefixes);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// We keep references to channels in their buffers,
+// and weak references in their users and the name lookup table.
+
+struct channel
+{
+ REF_COUNTABLE_HEADER
+
+ struct server *s; ///< Server
+
+ char *name; ///< Channel name
+ char *topic; ///< Channel topic
+
+ // XXX: write something like an ordered set of characters object?
+ struct str no_param_modes; ///< No parameter channel modes
+ struct str_map param_modes; ///< Parametrized channel modes
+
+ struct channel_user *users; ///< Channel users
+ struct strv names_buf; ///< Buffer for RPL_NAMREPLY
+ size_t users_len; ///< User count
+
+ bool left_manually; ///< Don't rejoin on reconnect
+ bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES
+};
+
+static struct ispect_field g_channel_ispect[] =
+{
+ ISPECT( channel, name, STRING )
+ ISPECT( channel, topic, STRING )
+ ISPECT( channel, no_param_modes, STR )
+ ISPECT_MAP( channel, param_modes, STRING )
+ ISPECT( channel, users_len, SIZE )
+ ISPECT( channel, left_manually, BOOL )
+ {}
+};
+
+static struct channel *
+channel_new (struct server *s, char *name)
+{
+ struct channel *self = xcalloc (1, sizeof *self);
+ self->ref_count = 1;
+ self->s = s;
+ self->name = name;
+ self->no_param_modes = str_make ();
+ self->param_modes = str_map_make (free);
+ self->names_buf = strv_make ();
+ return self;
+}
+
+static void
+channel_destroy (struct channel *self)
+{
+ free (self->name);
+ free (self->topic);
+ str_free (&self->no_param_modes);
+ str_map_free (&self->param_modes);
+ // Owner has to make sure we have no users by now
+ hard_assert (!self->users && !self->users_len);
+ strv_free (&self->names_buf);
+ free (self);
+}
+
+REF_COUNTABLE_METHODS (channel)
+
+// ~~~ Attribute utilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+enum
+{
+ TEXT_BOLD = 1 << 0,
+ TEXT_ITALIC = 1 << 1,
+ TEXT_UNDERLINE = 1 << 2,
+ TEXT_INVERSE = 1 << 3,
+ TEXT_BLINK = 1 << 4,
+ TEXT_CROSSED_OUT = 1 << 5,
+ TEXT_MONOSPACE = 1 << 6
+};
+
+// Similar to code in liberty-tui.c.
+struct attrs
+{
+ short fg; ///< Foreground (256-colour cube or -1)
+ short bg; ///< Background (256-colour cube or -1)
+ unsigned attrs; ///< TEXT_* mask
+};
+
+/// Decode attributes in the value using a subset of the git config format,
+/// ignoring all errors since it doesn't affect functionality
+static struct attrs
+attrs_decode (const char *value)
+{
+ struct strv v = strv_make ();
+ cstr_split (value, " ", true, &v);
+
+ int colors = 0;
+ struct attrs attrs = { -1, -1, 0 };
+ for (char **it = v.vector; *it; it++)
+ {
+ char *end = NULL;
+ long n = strtol (*it, &end, 10);
+ if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
+ {
+ if (colors == 0) attrs.fg = n;
+ if (colors == 1) attrs.bg = n;
+ colors++;
+ }
+ else if (!strcmp (*it, "bold")) attrs.attrs |= TEXT_BOLD;
+ else if (!strcmp (*it, "italic")) attrs.attrs |= TEXT_ITALIC;
+ else if (!strcmp (*it, "ul")) attrs.attrs |= TEXT_UNDERLINE;
+ else if (!strcmp (*it, "reverse")) attrs.attrs |= TEXT_INVERSE;
+ else if (!strcmp (*it, "blink")) attrs.attrs |= TEXT_BLINK;
+ else if (!strcmp (*it, "strike")) attrs.attrs |= TEXT_CROSSED_OUT;
+ }
+ strv_free (&v);
+ return attrs;
+}
+
+// ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+enum formatter_item_type
+{
+ FORMATTER_ITEM_END, ///< Sentinel value for arrays
+ FORMATTER_ITEM_TEXT, ///< Text
+ FORMATTER_ITEM_ATTR, ///< Formatting attributes
+ FORMATTER_ITEM_FG_COLOR, ///< Foreground colour
+ FORMATTER_ITEM_BG_COLOR, ///< Background colour
+ FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting
+ FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
+};
+
+struct formatter_item
+{
+ enum formatter_item_type type : 16; ///< Type of this item
+ int attribute : 16; ///< Attribute ID or a TEXT_* mask
+ int color; ///< Colour ([256 << 16] | 16)
+ char *text; ///< String
+};
+
+static void
+formatter_item_free (struct formatter_item *self)
+{
+ free (self->text);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct formatter
+{
+ struct app_context *ctx; ///< Application context
+ struct server *s; ///< Server
+
+ bool clean; ///< Assume ATTR_RESET
+ struct formatter_item *items; ///< Items
+ size_t items_len; ///< Items used
+ size_t items_alloc; ///< Items allocated
+};
+
+static struct formatter
+formatter_make (struct app_context *ctx, struct server *s)
+{
+ struct formatter self = { .ctx = ctx, .s = s, .clean = true };
+ self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16));
+ return self;
+}
+
+static void
+formatter_free (struct formatter *self)
+{
+ for (size_t i = 0; i < self->items_len; i++)
+ formatter_item_free (&self->items[i]);
+ free (self->items);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum buffer_line_flags
+{
+ BUFFER_LINE_SKIP_FILE = 1 << 0, ///< Don't log this to file
+ BUFFER_LINE_UNIMPORTANT = 1 << 1, ///< Joins, parts, similar spam
+ BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this
+};
+
+// NOTE: This sequence must match up with xC-proto, only one lower.
+enum buffer_line_rendition
+{
+ BUFFER_LINE_BARE, ///< Unadorned
+ BUFFER_LINE_INDENT, ///< Just indent the line
+ BUFFER_LINE_STATUS, ///< Status message
+ BUFFER_LINE_ERROR, ///< Error message
+ BUFFER_LINE_JOIN, ///< Join arrow
+ BUFFER_LINE_PART, ///< Part arrow
+ BUFFER_LINE_ACTION, ///< Highlighted asterisk
+};
+
+struct buffer_line
+{
+ LIST_HEADER (struct buffer_line)
+
+ unsigned flags; ///< Functional flags
+ enum buffer_line_rendition r; ///< What the line should look like
+ time_t when; ///< Time of the event
+ struct formatter_item items[]; ///< Line data
+};
+
+/// Create a new buffer line stealing all data from the provided formatter
+struct buffer_line *
+buffer_line_new (struct formatter *f)
+{
+ // We make space for one more item that gets initialized to all zeros,
+ // meaning FORMATTER_ITEM_END (because it's the first value in the enum)
+ size_t items_size = f->items_len * sizeof *f->items;
+ struct buffer_line *self =
+ xcalloc (1, sizeof *self + items_size + sizeof *self->items);
+ memcpy (self->items, f->items, items_size);
+
+ // We've stolen pointers from the formatter, let's destroy it altogether
+ free (f->items);
+ memset (f, 0, sizeof *f);
+ return self;
+}
+
+static void
+buffer_line_destroy (struct buffer_line *self)
+{
+ for (struct formatter_item *iter = self->items; iter->type; iter++)
+ formatter_item_free (iter);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum buffer_type
+{
+ BUFFER_GLOBAL, ///< Global information
+ BUFFER_SERVER, ///< Server-related messages
+ BUFFER_CHANNEL, ///< Channels
+ BUFFER_PM ///< Private messages (query)
+};
+
+struct buffer
+{
+ LIST_HEADER (struct buffer)
+ REF_COUNTABLE_HEADER
+
+ enum buffer_type type; ///< Type of the buffer
+ char *name; ///< The name of the buffer
+
+ struct input *input; ///< API for "input_data"
+ input_buffer_t input_data; ///< User interface data
+
+ // Buffer contents:
+
+ struct buffer_line *lines; ///< All lines in this buffer
+ struct buffer_line *lines_tail; ///< The tail of buffer lines
+ unsigned lines_count; ///< How many lines we have
+
+ unsigned new_messages_count; ///< # messages since last left
+ unsigned new_unimportant_count; ///< How much of that is unimportant
+ bool highlighted; ///< We've been highlighted
+ bool hide_unimportant; ///< Hide unimportant messages
+
+ FILE *log_file; ///< Log file
+
+ // Origin information:
+
+ struct server *server; ///< Reference to server
+ struct channel *channel; ///< Reference to channel
+ struct user *user; ///< Reference to user
+};
+
+static struct ispect_field g_server_ispect[];
+static struct ispect_field g_buffer_ispect[] =
+{
+ ISPECT( buffer, name, STRING )
+ ISPECT( buffer, new_messages_count, UINT )
+ ISPECT( buffer, new_unimportant_count, UINT )
+ ISPECT( buffer, highlighted, BOOL )
+ ISPECT( buffer, hide_unimportant, BOOL )
+ ISPECT_REF( buffer, server, false, server )
+ ISPECT_REF( buffer, channel, false, channel )
+ ISPECT_REF( buffer, user, false, user )
+ {}
+};
+
+static struct buffer *
+buffer_new (struct input *input, enum buffer_type type, char *name)
+{
+ struct buffer *self = xcalloc (1, sizeof *self);
+ self->ref_count = 1;
+ self->input = input;
+ self->input_data = CALL (input, buffer_new);
+ self->type = type;
+ self->name = name;
+ return self;
+}
+
+static void
+buffer_destroy (struct buffer *self)
+{
+ free (self->name);
+ if (self->input_data)
+ {
+#ifdef HAVE_READLINE
+ // FIXME: can't really free "history" contents from here, as we cannot
+ // be sure that the user interface pointer is valid and usable
+ input_rl__buffer_destroy_wo_history (self->input_data);
+#else // ! HAVE_READLINE
+ CALL_ (self->input, buffer_destroy, self->input_data);
+#endif // ! HAVE_READLINE
+ }
+ LIST_FOR_EACH (struct buffer_line, iter, self->lines)
+ buffer_line_destroy (iter);
+ if (self->log_file)
+ (void) fclose (self->log_file);
+ if (self->user)
+ user_unref (self->user);
+ if (self->channel)
+ channel_unref (self->channel);
+ free (self);
+}
+
+REF_COUNTABLE_METHODS (buffer)
+#define buffer_ref do_not_use_dangerous
+
+// ~~~ Relay ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+struct client
+{
+ LIST_HEADER (struct client)
+ struct app_context *ctx; ///< Application context
+
+ // TODO: Convert this all to TLS, and only TLS, with required client cert.
+ // That means replacing plumbing functions with the /other/ set from xD.
+
+ int socket_fd; ///< The TCP socket
+ struct str read_buffer; ///< Unprocessed input
+ struct str write_buffer; ///< Output yet to be sent out
+
+ uint32_t event_seq; ///< Outgoing message counter
+ bool initialized; ///< Initial sync took place
+
+ struct poller_fd socket_event; ///< The socket can be read/written to
+};
+
+static struct client *
+client_new (void)
+{
+ struct client *self = xcalloc (1, sizeof *self);
+ self->socket_fd = -1;
+ self->read_buffer = str_make ();
+ self->write_buffer = str_make ();
+ return self;
+}
+
+static void
+client_destroy (struct client *self)
+{
+ if (!soft_assert (self->socket_fd == -1))
+ xclose (self->socket_fd);
+
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+ free (self);
+}
+
+static void client_kill (struct client *c);
+
+// ~~~ Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+// The only real purpose of this is to abstract away TLS
+struct transport
+{
+ /// Initialize the transport
+ bool (*init) (struct server *s, const char *hostname, struct error **e);
+ /// Destroy the user data pointer
+ void (*cleanup) (struct server *s);
+
+ /// The underlying socket may have become readable, update `read_buffer'
+ enum socket_io_result (*try_read) (struct server *s);
+ /// The underlying socket may have become writeable, flush `write_buffer'
+ enum socket_io_result (*try_write) (struct server *s);
+ /// Return event mask to use in the poller
+ int (*get_poll_events) (struct server *s);
+
+ /// Called just before closing the connection from our side
+ void (*in_before_shutdown) (struct server *s);
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum server_state
+{
+ IRC_DISCONNECTED, ///< Not connected
+ IRC_CONNECTING, ///< Connecting to the server
+ IRC_CONNECTED, ///< Trying to register
+ IRC_REGISTERED, ///< We can chat now
+ IRC_CLOSING, ///< Flushing output before shutdown
+ IRC_HALF_CLOSED ///< Connection shutdown from our side
+};
+
+/// Convert an IRC identifier character to lower-case
+typedef int (*irc_tolower_fn) (int);
+
+/// Key conversion function for hashmap lookups
+typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t);
+
+struct server
+{
+ REF_COUNTABLE_HEADER
+ struct app_context *ctx; ///< Application context
+
+ char *name; ///< Server identifier
+ struct buffer *buffer; ///< The buffer for this server
+ struct config_item *config; ///< Configuration root
+
+ // Connection:
+
+ enum server_state state; ///< Connection state
+ struct connector *connector; ///< Connection establisher
+ struct socks_connector *socks_conn; ///< SOCKS connection establisher
+ unsigned reconnect_attempt; ///< Number of reconnect attempt
+ bool manual_disconnect; ///< Don't reconnect after disconnect
+
+ int socket; ///< Socket FD of the server
+ struct str read_buffer; ///< Input yet to be processed
+ struct str write_buffer; ///< Outut yet to be be sent out
+ struct poller_fd socket_event; ///< We can read from the socket
+
+ struct transport *transport; ///< Transport method
+ void *transport_data; ///< Transport data
+
+ // Events:
+
+ struct poller_timer ping_tmr; ///< We should send a ping
+ struct poller_timer timeout_tmr; ///< Connection seems to be dead
+ struct poller_timer reconnect_tmr; ///< We should reconnect now
+ struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate
+
+ // IRC:
+
+ // TODO: an output queue to prevent excess floods (this will be needed
+ // especially for away status polling)
+
+ bool rehashing; ///< Rehashing IRC identifiers
+
+ struct str_map irc_users; ///< IRC user data
+ struct str_map irc_channels; ///< IRC channel data
+ struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers
+
+ struct user *irc_user; ///< Our own user
+ int nick_counter; ///< Iterates "nicks" when registering
+ struct str irc_user_modes; ///< Our current user modes
+ char *irc_user_host; ///< Our current user@host
+ bool autoaway_active; ///< Autoaway is currently active
+
+ struct strv outstanding_joins; ///< JOINs we expect a response to
+
+ struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS
+ bool cap_echo_message; ///< Whether the server echoes messages
+ bool cap_away_notify; ///< Whether we get AWAY notifications
+ bool cap_sasl; ///< Whether SASL is available
+
+ // Server-specific information (from RPL_ISUPPORT):
+
+ irc_tolower_fn irc_tolower; ///< Server tolower()
+ irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm()
+
+ char *irc_chantypes; ///< Channel types (name prefixes)
+ char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
+ char *irc_statusmsg; ///< Prefixes for channel targets
+
+ char irc_extban_prefix; ///< EXTBAN prefix or \0
+ char *irc_extban_types; ///< EXTBAN types
+
+ char *irc_chanmodes_list; ///< Channel modes for lists
+ char *irc_chanmodes_param_always; ///< Channel modes with mandatory param
+ char *irc_chanmodes_param_when_set; ///< Channel modes with param when set
+ char *irc_chanmodes_param_never; ///< Channel modes without param
+
+ char *irc_chanuser_prefixes; ///< Channel user prefixes
+ char *irc_chanuser_modes; ///< Channel user modes
+
+ unsigned irc_max_modes; ///< Max parametrized modes per command
+};
+
+static struct ispect_field g_server_ispect[] =
+{
+ ISPECT( server, name, STRING )
+ ISPECT( server, state, INT )
+ ISPECT( server, reconnect_attempt, UINT )
+ ISPECT( server, manual_disconnect, BOOL )
+ ISPECT( server, irc_user_host, STRING )
+ ISPECT( server, autoaway_active, BOOL )
+ ISPECT( server, cap_echo_message, BOOL )
+ ISPECT_REF( server, buffer, false, buffer )
+
+ // TODO: either rename the underlying field or fix the plugins
+ { "user", offsetof (struct server, irc_user),
+ ISPECT_REF, 0, g_user_ispect, false },
+ { "user_mode", offsetof (struct server, irc_user_modes),
+ ISPECT_STR, 0, NULL, false },
+
+ {}
+};
+
+static void on_irc_timeout (void *user_data);
+static void on_irc_ping_timeout (void *user_data);
+static void on_irc_autojoin_timeout (void *user_data);
+static void irc_initiate_connect (struct server *s);
+
+static void
+server_init_specifics (struct server *self)
+{
+ // Defaults as per the RPL_ISUPPORT drafts, or RFC 1459
+
+ self->irc_tolower = irc_tolower;
+ self->irc_strxfrm = irc_strxfrm;
+
+ self->irc_chantypes = xstrdup ("#&");
+ self->irc_idchan_prefixes = xstrdup ("");
+ self->irc_statusmsg = xstrdup ("");
+
+ self->irc_extban_prefix = 0;
+ self->irc_extban_types = xstrdup ("");
+
+ self->irc_chanmodes_list = xstrdup ("b");
+ self->irc_chanmodes_param_always = xstrdup ("k");
+ self->irc_chanmodes_param_when_set = xstrdup ("l");
+ self->irc_chanmodes_param_never = xstrdup ("imnpst");
+
+ self->irc_chanuser_prefixes = xstrdup ("@+");
+ self->irc_chanuser_modes = xstrdup ("ov");
+
+ self->irc_max_modes = 3;
+}
+
+static void
+server_free_specifics (struct server *self)
+{
+ free (self->irc_chantypes);
+ free (self->irc_idchan_prefixes);
+ free (self->irc_statusmsg);
+
+ free (self->irc_extban_types);
+
+ free (self->irc_chanmodes_list);
+ free (self->irc_chanmodes_param_always);
+ free (self->irc_chanmodes_param_when_set);
+ free (self->irc_chanmodes_param_never);
+
+ free (self->irc_chanuser_prefixes);
+ free (self->irc_chanuser_modes);
+}
+
+static struct server *
+server_new (struct poller *poller)
+{
+ struct server *self = xcalloc (1, sizeof *self);
+ self->ref_count = 1;
+
+ self->socket = -1;
+ self->read_buffer = str_make ();
+ self->write_buffer = str_make ();
+ self->state = IRC_DISCONNECTED;
+
+ self->timeout_tmr = poller_timer_make (poller);
+ self->timeout_tmr.dispatcher = on_irc_timeout;
+ self->timeout_tmr.user_data = self;
+
+ self->ping_tmr = poller_timer_make (poller);
+ self->ping_tmr.dispatcher = on_irc_ping_timeout;
+ self->ping_tmr.user_data = self;
+
+ self->reconnect_tmr = poller_timer_make (poller);
+ self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
+ self->reconnect_tmr.user_data = self;
+
+ self->autojoin_tmr = poller_timer_make (poller);
+ self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
+ self->autojoin_tmr.user_data = self;
+
+ self->irc_users = str_map_make (NULL);
+ self->irc_users.key_xfrm = irc_strxfrm;
+ self->irc_channels = str_map_make (NULL);
+ self->irc_channels.key_xfrm = irc_strxfrm;
+ self->irc_buffer_map = str_map_make (NULL);
+ self->irc_buffer_map.key_xfrm = irc_strxfrm;
+
+ self->irc_user_modes = str_make ();
+
+ self->outstanding_joins = strv_make ();
+ self->cap_ls_buf = strv_make ();
+ server_init_specifics (self);
+ return self;
+}
+
+static void
+server_destroy (struct server *self)
+{
+ free (self->name);
+
+ if (self->connector)
+ {
+ connector_free (self->connector);
+ free (self->connector);
+ }
+ if (self->socks_conn)
+ {
+ socks_connector_free (self->socks_conn);
+ free (self->socks_conn);
+ }
+
+ if (self->transport
+ && self->transport->cleanup)
+ self->transport->cleanup (self);
+
+ if (self->socket != -1)
+ {
+ poller_fd_reset (&self->socket_event);
+ xclose (self->socket);
+ }
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+
+ poller_timer_reset (&self->ping_tmr);
+ poller_timer_reset (&self->timeout_tmr);
+ poller_timer_reset (&self->reconnect_tmr);
+ poller_timer_reset (&self->autojoin_tmr);
+
+ str_map_free (&self->irc_users);
+ str_map_free (&self->irc_channels);
+ str_map_free (&self->irc_buffer_map);
+
+ if (self->irc_user)
+ user_unref (self->irc_user);
+ str_free (&self->irc_user_modes);
+ free (self->irc_user_host);
+
+ strv_free (&self->outstanding_joins);
+ strv_free (&self->cap_ls_buf);
+ server_free_specifics (self);
+ free (self);
+}
+
+REF_COUNTABLE_METHODS (server)
+#define server_ref do_not_use_dangerous
+
+// ~~~ Scripting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+struct plugin
+{
+ LIST_HEADER (struct plugin)
+
+ char *name; ///< Name of the plugin
+ struct plugin_vtable *vtable; ///< Methods
+};
+
+struct plugin_vtable
+{
+ /// Collect garbage
+ void (*gc) (struct plugin *self);
+ /// Unregister and free the plugin including all relevant resources
+ void (*free) (struct plugin *self);
+};
+
+static void
+plugin_destroy (struct plugin *self)
+{
+ self->vtable->free (self);
+ free (self->name);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// This is a bit ugly since insertion is O(n) and the need to get rid of the
+// specific type because of list macros, however I don't currently posses any
+// strictly better, ordered data structure
+
+struct hook
+{
+ LIST_HEADER (struct hook)
+ int priority; ///< The lesser the sooner
+};
+
+static struct hook *
+hook_insert (struct hook *list, struct hook *item)
+{
+ // Corner cases: list is empty or we precede everything
+ if (!list || item->priority < list->priority)
+ {
+ LIST_PREPEND (list, item);
+ return list;
+ }
+
+ // Otherwise fast-forward to the last entry that precedes us
+ struct hook *before = list;
+ while (before->next && before->next->priority < item->priority)
+ before = before->next;
+
+ // And link ourselves in between it and its successor
+ if ((item->next = before->next))
+ item->next->prev = item;
+ before->next = item;
+ item->prev = before;
+ return list;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct input_hook
+{
+ struct hook super; ///< Common hook fields
+
+ /// Takes over the ownership of "input", returns either NULL if input
+ /// was thrown away, or a possibly modified version of it
+ char *(*filter) (struct input_hook *self,
+ struct buffer *buffer, char *input);
+};
+
+struct irc_hook
+{
+ struct hook super; ///< Common hook fields
+
+ /// Takes over the ownership of "message", returns either NULL if message
+ /// was thrown away, or a possibly modified version of it
+ char *(*filter) (struct irc_hook *self,
+ struct server *server, char *message);
+};
+
+struct prompt_hook
+{
+ struct hook super; ///< Common hook fields
+
+ /// Returns what the prompt should look like right now based on other state
+ char *(*make) (struct prompt_hook *self);
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct completion_word
+{
+ size_t start; ///< Offset to start of word
+ size_t end; ///< Offset to end of word
+};
+
+struct completion
+{
+ char *line; ///< The line which is being completed
+
+ struct completion_word *words; ///< Word locations
+ size_t words_len; ///< Number of words
+ size_t words_alloc; ///< Number of words allocated
+
+ size_t location; ///< Which word is being completed
+};
+
+struct completion_hook
+{
+ struct hook super; ///< Common hook fields
+
+ /// Tries to add possible completions of "word" to "output"
+ void (*complete) (struct completion_hook *self,
+ struct completion *data, const char *word, struct strv *output);
+};
+
+// ~~~ Main context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+struct app_context
+{
+ /// Default terminal attributes
+ struct attrs theme_defaults[ATTR_COUNT];
+
+ // Configuration:
+
+ struct config config; ///< Program configuration
+ struct attrs theme[ATTR_COUNT]; ///< Terminal attributes
+ bool isolate_buffers; ///< Isolate global/server buffers
+ bool beep_on_highlight; ///< Beep on highlight
+ bool logging; ///< Logging to file enabled
+ bool show_all_prefixes; ///< Show all prefixes before nicks
+ bool word_wrapping; ///< Enable simple word wrapping
+
+ struct str_map servers; ///< Our servers
+
+ // Relay:
+
+ int relay_fd; ///< Listening socket FD
+ struct client *clients; ///< Our relay clients
+
+ /// A single message buffer to prepare all outcoming messages within
+ struct relay_event_message relay_message;
+
+ // Events:
+
+ struct poller_fd tty_event; ///< Terminal input event
+ struct poller_fd signal_event; ///< Signal FD event
+ struct poller_fd relay_event; ///< New relay connection available
+
+ struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
+ struct poller_timer date_chg_tmr; ///< Print a date change
+ struct poller_timer autoaway_tmr; ///< Autoaway timer
+
+ struct poller poller; ///< Manages polled descriptors
+ bool quitting; ///< User requested quitting
+ bool polling; ///< The event loop is running
+
+ // Buffers:
+
+ struct buffer *buffers; ///< All our buffers in order
+ struct buffer *buffers_tail; ///< The tail of our buffers
+
+ struct buffer *global_buffer; ///< The global buffer
+ struct buffer *current_buffer; ///< The current buffer
+ struct buffer *last_buffer; ///< Last used buffer
+
+ struct str_map buffers_by_name; ///< Buffers by name
+
+ unsigned backlog_limit; ///< Limit for buffer lines
+ time_t last_displayed_msg_time; ///< Time of last displayed message
+
+ // Terminal:
+
+ iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
+ iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
+
+ struct input *input; ///< User interface
+
+ struct poller_idle prompt_event; ///< Deferred prompt refresh
+ struct poller_idle input_event; ///< Pending input event
+ struct strv pending_input; ///< Pending input lines
+
+ int *nick_palette; ///< A 256-colour palette for nicknames
+ size_t nick_palette_len; ///< Number of entries in nick_palette
+
+ bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape
+ bool in_bracketed_paste; ///< User is pasting some content
+ struct str input_buffer; ///< Buffered pasted content
+
+ bool running_pager; ///< Running a pager for buffer history
+ bool running_editor; ///< Running editor for the input
+ char *editor_filename; ///< The file being edited by user
+ int terminal_suspended; ///< Terminal suspension level
+
+ // Plugins:
+
+ struct plugin *plugins; ///< Loaded plugins
+ struct hook *input_hooks; ///< Input hooks
+ struct hook *irc_hooks; ///< IRC hooks
+ struct hook *prompt_hooks; ///< Prompt hooks
+ struct hook *completion_hooks; ///< Autocomplete hooks
+}
+*g_ctx;
+
+static struct ispect_field g_ctx_ispect[] =
+{
+ ISPECT_MAP_REF( app_context, servers, false, server )
+ ISPECT_REF( app_context, buffers, true, buffer )
+ ISPECT_REF( app_context, global_buffer, false, buffer )
+ ISPECT_REF( app_context, current_buffer, false, buffer )
+ {}
+};
+
+static int *
+filter_color_cube_for_acceptable_nick_colors (size_t *len)
+{
+ // This is a pure function and we don't use threads, static storage is fine
+ static int table[6 * 6 * 6];
+ size_t len_counter = 0;
+ for (int x = 0; x < (int) N_ELEMENTS (table); x++)
+ {
+ int r = x / 36;
+ int g = (x / 6) % 6;
+ int b = (x % 6);
+
+ // The first step is 95/255, the rest are 40/255,
+ // as an approximation we can double the first step
+ double linear_R = pow ((r + !!r) / 6., 2.2);
+ double linear_G = pow ((g + !!g) / 6., 2.2);
+ double linear_B = pow ((b + !!b) / 6., 2.2);
+
+ // Use the relative luminance of colours within the cube to filter
+ // colours that look okay-ish on terminals with both black and white
+ // backgrounds (use the test-nick-colors script to calibrate)
+ double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
+ if (Y >= .25 && Y <= .4)
+ table[len_counter++] = 16 + x;
+ }
+ *len = len_counter;
+ return table;
+}
+
+static bool
+app_iconv_open (iconv_t *target, const char *to, const char *from)
+{
+ if (ICONV_ACCEPTS_TRANSLIT)
+ {
+ char *to_real = xstrdup_printf ("%s//TRANSLIT", to);
+ *target = iconv_open (to_real, from);
+ free (to_real);
+ }
+ else
+ *target = iconv_open (to, from);
+ return *target != (iconv_t) -1;
+}
+
+static void
+app_context_init (struct app_context *self)
+{
+ memset (self, 0, sizeof *self);
+
+ self->config = config_make ();
+ poller_init (&self->poller);
+
+ self->relay_fd = -1;
+
+ self->servers = str_map_make ((str_map_free_fn) server_unref);
+ self->servers.key_xfrm = tolower_ascii_strxfrm;
+
+ self->buffers_by_name = str_map_make (NULL);
+ self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
+
+ // So that we don't lose the logo shortly after startup
+ self->backlog_limit = 1000;
+ self->last_displayed_msg_time = time (NULL);
+
+ char *native = nl_langinfo (CODESET);
+ if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8")
+ || !app_iconv_open (&self->term_to_utf8, "UTF-8", native))
+ exit_fatal ("creating the UTF-8 conversion object failed: %s",
+ strerror (errno));
+
+ self->input = input_new ();
+ self->input->user_data = self;
+ self->pending_input = strv_make ();
+ self->input_buffer = str_make ();
+
+ self->nick_palette =
+ filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
+}
+
+static void
+app_context_relay_stop (struct app_context *self)
+{
+ if (self->relay_fd != -1)
+ {
+ poller_fd_reset (&self->relay_event);
+ xclose (self->relay_fd);
+ self->relay_fd = -1;
+ }
+}
+
+static void
+app_context_free (struct app_context *self)
+{
+ // Plugins can try to use of the other fields when destroyed
+ LIST_FOR_EACH (struct plugin, iter, self->plugins)
+ plugin_destroy (iter);
+
+ config_free (&self->config);
+
+ LIST_FOR_EACH (struct buffer, iter, self->buffers)
+ {
+#ifdef HAVE_READLINE
+ // We can use the user interface here; see buffer_destroy()
+ CALL_ (self->input, buffer_destroy, iter->input_data);
+ iter->input_data = NULL;
+#endif // HAVE_READLINE
+ buffer_unref (iter);
+ }
+ str_map_free (&self->buffers_by_name);
+
+ app_context_relay_stop (self);
+ LIST_FOR_EACH (struct client, c, self->clients)
+ client_kill (c);
+ relay_event_message_free (&self->relay_message);
+
+ str_map_free (&self->servers);
+ poller_free (&self->poller);
+
+ iconv_close (self->term_from_utf8);
+ iconv_close (self->term_to_utf8);
+
+ CALL (self->input, destroy);
+ strv_free (&self->pending_input);
+ str_free (&self->input_buffer);
+
+ free (self->editor_filename);
+}
+
+static void
+refresh_prompt (struct app_context *ctx)
+{
+ // XXX: the need for this conditional could probably be resolved
+ // by some clever reordering
+ if (ctx->prompt_event.poller)
+ poller_idle_set (&ctx->prompt_event);
+}
+
+// --- Configuration -----------------------------------------------------------
+
+static void
+on_config_debug_mode_change (struct config_item *item)
+{
+ g_debug_mode = item->value.boolean;
+}
+
+static void
+on_config_show_all_prefixes_change (struct config_item *item)
+{
+ struct app_context *ctx = item->user_data;
+ ctx->show_all_prefixes = item->value.boolean;
+ refresh_prompt (ctx);
+}
+
+static void on_config_relay_bind_change (struct config_item *item);
+static void on_config_backlog_limit_change (struct config_item *item);
+static void on_config_theme_change (struct config_item *item);
+static void on_config_logging_change (struct config_item *item);
+
+#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \
+ static void \
+ on_config_ ## name ## _change (struct config_item *item) \
+ { \
+ struct app_context *ctx = item->user_data; \
+ ctx->name = item->value.boolean; \
+ }
+
+TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers)
+TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight)
+TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping)
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+config_validate_nonjunk_string
+ (const struct config_item *item, struct error **e)
+{
+ if (item->type == CONFIG_ITEM_NULL)
+ return true;
+
+ hard_assert (config_item_type_is_string (item->type));
+ for (size_t i = 0; i < item->value.string.len; i++)
+ {
+ // Not even a tabulator
+ unsigned char c = item->value.string.str[i];
+ if (iscntrl_ascii (c))
+ {
+ error_set (e, "control characters are not allowed");
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+config_validate_addresses
+ (const struct config_item *item, struct error **e)
+{
+ if (item->type == CONFIG_ITEM_NULL)
+ return true;
+ if (!config_validate_nonjunk_string (item, e))
+ return false;
+
+ // Comma-separated list of "host[:port]" pairs
+ regex_t re;
+ int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?"
+ "(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB);
+ hard_assert (!err);
+
+ bool result = !regexec (&re, item->value.string.str, 0, NULL, 0);
+ if (!result)
+ error_set (e, "invalid address list string");
+
+ regfree (&re);
+ return result;
+}
+
+static bool
+config_validate_nonnegative
+ (const struct config_item *item, struct error **e)
+{
+ if (item->type == CONFIG_ITEM_NULL)
+ return true;
+
+ hard_assert (item->type == CONFIG_ITEM_INTEGER);
+ if (item->value.integer >= 0)
+ return true;
+
+ error_set (e, "must be non-negative");
+ return false;
+}
+
+static struct config_schema g_config_server[] =
+{
+ { .name = "nicks",
+ .comment = "IRC nickname",
+ .type = CONFIG_ITEM_STRING_ARRAY,
+ .validate = config_validate_nonjunk_string },
+ { .name = "username",
+ .comment = "IRC user name",
+ .type = CONFIG_ITEM_STRING,
+ .validate = config_validate_nonjunk_string },
+ { .name = "realname",
+ .comment = "IRC real name/e-mail",
+ .type = CONFIG_ITEM_STRING,
+ .validate = config_validate_nonjunk_string },
+
+ { .name = "addresses",
+ .comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
+ .type = CONFIG_ITEM_STRING_ARRAY,
+ .validate = config_validate_addresses },
+ { .name = "password",
+ .comment = "Password to connect to the server, if any",
+ .type = CONFIG_ITEM_STRING,
+ .validate = config_validate_nonjunk_string },
+ // XXX: if we add support for new capabilities, the value stays unchanged
+ { .name = "capabilities",
+ .comment = "Capabilities to use if supported by server",
+ .type = CONFIG_ITEM_STRING_ARRAY,
+ .validate = config_validate_nonjunk_string,
+ .default_ = "\"multi-prefix,invite-notify,server-time,echo-message,"
+ "message-tags,away-notify,cap-notify,chghost\"" },
+
+ { .name = "tls",
+ .comment = "Whether to use TLS",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off" },
+ { .name = "tls_cert",
+ .comment = "Client TLS certificate (PEM)",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "tls_verify",
+ .comment = "Whether to verify certificates",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on" },
+ { .name = "tls_ca_file",
+ .comment = "OpenSSL CA bundle file",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "tls_ca_path",
+ .comment = "OpenSSL CA bundle path",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "tls_ciphers",
+ .comment = "OpenSSL cipher preference list",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "\"DEFAULT:!MEDIUM:!LOW\"" },
+
+ { .name = "autoconnect",
+ .comment = "Connect automatically on startup",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on" },
+ { .name = "autojoin",
+ .comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")",
+ .type = CONFIG_ITEM_STRING_ARRAY,
+ .validate = config_validate_nonjunk_string },
+ { .name = "command",
+ .comment = "Command to execute after a successful connect",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "command_delay",
+ .comment = "Delay between executing \"command\" and joining channels",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "0" },
+ { .name = "reconnect",
+ .comment = "Whether to reconnect on error",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on" },
+ { .name = "reconnect_delay",
+ .comment = "Time between reconnecting",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "5" },
+
+ { .name = "socks_host",
+ .comment = "Address of a SOCKS 4a/5 proxy",
+ .type = CONFIG_ITEM_STRING,
+ .validate = config_validate_nonjunk_string },
+ { .name = "socks_port",
+ .comment = "SOCKS port number",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "1080" },
+ { .name = "socks_username",
+ .comment = "SOCKS auth. username",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "socks_password",
+ .comment = "SOCKS auth. password",
+ .type = CONFIG_ITEM_STRING },
+ {}
+};
+
+static struct config_schema g_config_general[] =
+{
+ { .name = "autosave",
+ .comment = "Save configuration automatically after each change",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on" },
+ { .name = "debug_mode",
+ .comment = "Produce some debugging output",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off",
+ .on_change = on_config_debug_mode_change },
+ { .name = "logging",
+ .comment = "Log buffer contents to file",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off",
+ .on_change = on_config_logging_change },
+ { .name = "plugin_autoload",
+ .comment = "Plugins to automatically load on start",
+ .type = CONFIG_ITEM_STRING_ARRAY,
+ .validate = config_validate_nonjunk_string },
+ { .name = "relay_bind",
+ .comment = "Address to bind to for a user interface relay point",
+ .type = CONFIG_ITEM_STRING,
+ .validate = config_validate_nonjunk_string,
+ .on_change = on_config_relay_bind_change },
+
+ // Buffer history:
+ { .name = "backlog_limit",
+ .comment = "Maximum number of lines stored in the backlog",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "1000",
+ .on_change = on_config_backlog_limit_change },
+ { .name = "pager",
+ .comment = "Shell command to page buffer history (args: name [path])",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "`name=$(echo \"$1\" | sed 's/[%?:.]/\\\\&/g'); "
+ "prompt='?f%F:'$name'. ?db- page %db?L of %D. .(?eEND:?PB%PB\\%..)'; "
+ "LESSSECURE=1 less +Gb -Ps\"$prompt\" \"${2:--R}\"`" },
+ { .name = "pager_strip_formatting",
+ .comment = "Strip terminal formatting from pager input",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off" },
+
+ // Output adjustments:
+ { .name = "beep_on_highlight",
+ .comment = "Ring the bell when highlighted or on a new invisible PM",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on",
+ .on_change = on_config_beep_on_highlight_change },
+ { .name = "date_change_line",
+ .comment = "Input to strftime(3) for the date change line",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "\"%F\"" },
+ { .name = "isolate_buffers",
+ .comment = "Don't leak messages from the server and global buffers",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off",
+ .on_change = on_config_isolate_buffers_change },
+ { .name = "read_marker_char",
+ .comment = "The character to use for the read marker line",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "\"-\"",
+ .validate = config_validate_nonjunk_string },
+ { .name = "show_all_prefixes",
+ .comment = "Show all prefixes in front of nicknames",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "off",
+ .on_change = on_config_show_all_prefixes_change },
+ { .name = "word_wrapping",
+ .comment = "Enable simple word wrapping in buffers",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on",
+ .on_change = on_config_word_wrapping_change },
+
+ // User input:
+ { .name = "editor",
+ .comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\", "
+ "nano/micro/kakoune: \"nano/micro/kak +%L:%C %F\"",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "process_pasted_text",
+ .comment = "Normalize newlines and quote the command prefix in pastes",
+ .type = CONFIG_ITEM_BOOLEAN,
+ .default_ = "on" },
+
+ // Pan-server configuration:
+ { .name = "autoaway_message",
+ .comment = "Automated away message",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "\"I'm not here right now\"" },
+ { .name = "autoaway_delay",
+ .comment = "Delay from the last keypress in seconds",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "1800" },
+ { .name = "reconnect_delay_growing",
+ .comment = "Growth factor for the reconnect delay",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "2" },
+ { .name = "reconnect_delay_max",
+ .comment = "Maximum reconnect delay in seconds",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "600" },
+ {}
+};
+
+static struct config_schema g_config_theme[] =
+{
+#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \
+ .on_change = on_config_theme_change },
+ ATTR_TABLE (XX)
+#undef XX
+ {}
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+load_config_general (struct config_item *subtree, void *user_data)
+{
+ config_schema_apply_to_object (g_config_general, subtree, user_data);
+}
+
+static void
+load_config_theme (struct config_item *subtree, void *user_data)
+{
+ config_schema_apply_to_object (g_config_theme, subtree, user_data);
+}
+
+static void
+register_config_modules (struct app_context *ctx)
+{
+ struct config *config = &ctx->config;
+ // The servers are loaded later when we can create buffers for them
+ config_register_module (config, "servers", NULL, NULL);
+ config_register_module (config, "aliases", NULL, NULL);
+ config_register_module (config, "plugins", NULL, NULL);
+ config_register_module (config, "general", load_config_general, ctx);
+ config_register_module (config, "theme", load_config_theme, ctx);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *
+get_config_string (struct config_item *root, const char *key)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item);
+ if (item->type == CONFIG_ITEM_NULL)
+ return NULL;
+ hard_assert (config_item_type_is_string (item->type));
+ return item->value.string.str;
+}
+
+static bool
+set_config_string
+ (struct config_item *root, const char *key, const char *value)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item);
+
+ struct config_item *new_ = config_item_string_from_cstr (value);
+ struct error *e = NULL;
+ if (config_item_set_from (item, new_, &e))
+ return true;
+
+ config_item_destroy (new_);
+ print_error ("couldn't set `%s' in configuration: %s", key, e->message);
+ error_free (e);
+ return false;
+}
+
+static int64_t
+get_config_integer (struct config_item *root, const char *key)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
+ return item->value.integer;
+}
+
+static bool
+get_config_boolean (struct config_item *root, const char *key)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
+ return item->value.boolean;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct str_map *
+get_servers_config (struct app_context *ctx)
+{
+ return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
+}
+
+static struct str_map *
+get_aliases_config (struct app_context *ctx)
+{
+ return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
+}
+
+static struct str_map *
+get_plugins_config (struct app_context *ctx)
+{
+ return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+serialize_configuration (struct config_item *root, struct str *output)
+{
+ str_append (output,
+ "# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n"
+ "#\n"
+ "# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n"
+ "# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n"
+ "#\n"
+ "# Everything is in UTF-8. Any custom comments will be overwritten.\n"
+ "\n");
+
+ config_item_write (root, true, output);
+}
+
+// --- Relay output ------------------------------------------------------------
+
+static void
+client_kill (struct client *c)
+{
+ struct app_context *ctx = c->ctx;
+ poller_fd_reset (&c->socket_event);
+ xclose (c->socket_fd);
+ c->socket_fd = -1;
+
+ LIST_UNLINK (ctx->clients, c);
+ client_destroy (c);
+}
+
+static void
+client_update_poller (struct client *c, const struct pollfd *pfd)
+{
+ int new_events = POLLIN;
+ if (c->write_buffer.len)
+ new_events |= POLLOUT;
+
+ hard_assert (new_events != 0);
+ if (!pfd || pfd->events != new_events)
+ poller_fd_set (&c->socket_event, new_events);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+relay_send (struct client *c)
+{
+ struct relay_event_message *m = &c->ctx->relay_message;
+ m->event_seq = c->event_seq++;
+
+ // TODO: Also don't try sending anything if half-closed.
+ if (!c->initialized || c->socket_fd == -1)
+ return;
+
+ // liberty has msg_{reader,writer} already, but they use 8-byte lengths.
+ size_t frame_len_pos = c->write_buffer.len, frame_len = 0;
+ str_pack_u32 (&c->write_buffer, 0);
+ if (!relay_event_message_serialize (m, &c->write_buffer)
+ || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
+ {
+ print_error ("serialization failed, killing client");
+ client_kill (c);
+ return;
+ }
+
+ uint32_t len = htonl (frame_len);
+ memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
+ client_update_poller (c, NULL);
+}
+
+static void
+relay_broadcast (struct app_context *ctx)
+{
+ LIST_FOR_EACH (struct client, c, ctx->clients)
+ relay_send (c);
+}
+
+static struct relay_event_message *
+relay_prepare (struct app_context *ctx)
+{
+ struct relay_event_message *m = &ctx->relay_message;
+ relay_event_message_free (m);
+ memset (m, 0, sizeof *m);
+ return m;
+}
+
+static void
+relay_prepare_ping (struct app_context *ctx)
+{
+ relay_prepare (ctx)->data.event = RELAY_EVENT_PING;
+}
+
+static union relay_item_data *
+relay_translate_formatter (struct app_context *ctx, union relay_item_data *p,
+ const struct formatter_item *i)
+{
+ // XXX: See attr_printer_decode_color(), this is a footgun.
+ int16_t c16 = i->color;
+ int16_t c256 = i->color >> 16;
+
+ unsigned attrs = i->attribute;
+ switch (i->type)
+ {
+ case FORMATTER_ITEM_TEXT:
+ p->text.text = str_from_cstr (i->text);
+ (p++)->kind = RELAY_ITEM_TEXT;
+ break;
+ case FORMATTER_ITEM_FG_COLOR:
+ p->fg_color.color = c256 <= 0 ? c16 : c256;
+ (p++)->kind = RELAY_ITEM_FG_COLOR;
+ break;
+ case FORMATTER_ITEM_BG_COLOR:
+ p->bg_color.color = c256 <= 0 ? c16 : c256;
+ (p++)->kind = RELAY_ITEM_BG_COLOR;
+ break;
+ case FORMATTER_ITEM_ATTR:
+ (p++)->kind = RELAY_ITEM_RESET;
+ if ((c256 = ctx->theme[i->attribute].fg) >= 0)
+ {
+ p->fg_color.color = c256;
+ (p++)->kind = RELAY_ITEM_FG_COLOR;
+ }
+ if ((c256 = ctx->theme[i->attribute].bg) >= 0)
+ {
+ p->bg_color.color = c256;
+ (p++)->kind = RELAY_ITEM_BG_COLOR;
+ }
+
+ attrs = ctx->theme[i->attribute].attrs;
+ // Fall-through
+ case FORMATTER_ITEM_SIMPLE:
+ if (attrs & TEXT_BOLD)
+ (p++)->kind = RELAY_ITEM_FLIP_BOLD;
+ if (attrs & TEXT_ITALIC)
+ (p++)->kind = RELAY_ITEM_FLIP_ITALIC;
+ if (attrs & TEXT_UNDERLINE)
+ (p++)->kind = RELAY_ITEM_FLIP_UNDERLINE;
+ if (attrs & TEXT_INVERSE)
+ (p++)->kind = RELAY_ITEM_FLIP_INVERSE;
+ if (attrs & TEXT_CROSSED_OUT)
+ (p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT;
+ if (attrs & TEXT_MONOSPACE)
+ (p++)->kind = RELAY_ITEM_FLIP_MONOSPACE;
+ break;
+ default:
+ break;
+ }
+ return p;
+}
+
+static union relay_item_data *
+relay_items (struct app_context *ctx, const struct formatter_item *items,
+ uint32_t *len)
+{
+ size_t items_len = 0;
+ for (size_t i = 0; items[i].type; i++)
+ items_len++;
+
+ // Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
+ union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a;
+ for (const struct formatter_item *i = items; items_len--; i++)
+ p = relay_translate_formatter (ctx, p, i);
+
+ *len = p - a;
+ return a;
+}
+
+static void
+relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
+ struct buffer_line *line, bool leak_to_active)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_line *e = &m->data.buffer_line;
+ e->event = RELAY_EVENT_BUFFER_LINE;
+ e->buffer_name = str_from_cstr (buffer->name);
+ e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT);
+ e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT);
+ e->rendition = 1 + line->r;
+ e->when = line->when * 1000;
+ e->leak_to_active = leak_to_active;
+ e->items = relay_items (ctx, line->items, &e->items_len);
+}
+
+// TODO: Consider pushing this whole block of code much further down.
+static void formatter_add (struct formatter *self, const char *format, ...);
+static char *irc_to_utf8 (const char *text);
+
+static void
+relay_prepare_channel_buffer_update (struct app_context *ctx,
+ struct buffer *buffer, struct relay_buffer_context_channel *e)
+{
+ struct channel *channel = buffer->channel;
+ struct formatter f = formatter_make (ctx, buffer->server);
+ if (channel->topic)
+ formatter_add (&f, "#m", channel->topic);
+ e->topic = relay_items (ctx, f.items, &e->topic_len);
+ formatter_free (&f);
+
+ // As in make_prompt(), conceal the last known channel modes.
+ // XXX: This should use irc_channel_is_joined().
+ if (!channel->users_len)
+ return;
+
+ struct str modes = str_make ();
+ str_append_str (&modes, &channel->no_param_modes);
+
+ struct str params = str_make ();
+ struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
+ const char *param;
+ while ((param = str_map_iter_next (&iter)))
+ {
+ str_append_c (&modes, iter.link->key[0]);
+ str_append_c (&params, ' ');
+ str_append (&params, param);
+ }
+
+ str_append_str (&modes, &params);
+ str_free (&params);
+
+ char *modes_utf8 = irc_to_utf8 (modes.str);
+ str_free (&modes);
+ e->modes = str_from_cstr (modes_utf8);
+ free (modes_utf8);
+}
+
+static void
+relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_update *e = &m->data.buffer_update;
+ e->event = RELAY_EVENT_BUFFER_UPDATE;
+ e->buffer_name = str_from_cstr (buffer->name);
+ e->hide_unimportant = buffer->hide_unimportant;
+
+ struct str *server_name = NULL;
+ switch (buffer->type)
+ {
+ case BUFFER_GLOBAL:
+ e->context.kind = RELAY_BUFFER_KIND_GLOBAL;
+ break;
+ case BUFFER_SERVER:
+ e->context.kind = RELAY_BUFFER_KIND_SERVER;
+ server_name = &e->context.server.server_name;
+ break;
+ case BUFFER_CHANNEL:
+ e->context.kind = RELAY_BUFFER_KIND_CHANNEL;
+ server_name = &e->context.channel.server_name;
+ relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel);
+ break;
+ case BUFFER_PM:
+ e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE;
+ server_name = &e->context.private_message.server_name;
+ break;
+ }
+ if (server_name)
+ *server_name = str_from_cstr (buffer->server->name);
+}
+
+static void
+relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
+ e->event = RELAY_EVENT_BUFFER_STATS;
+ e->buffer_name = str_from_cstr (buffer->name);
+ e->new_messages = MIN (UINT32_MAX,
+ buffer->new_messages_count - buffer->new_unimportant_count);
+ e->new_unimportant_messages = MIN (UINT32_MAX,
+ buffer->new_unimportant_count);
+ e->highlighted = buffer->highlighted;
+}
+
+static void
+relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer,
+ const char *new_name)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_rename *e = &m->data.buffer_rename;
+ e->event = RELAY_EVENT_BUFFER_RENAME;
+ e->buffer_name = str_from_cstr (buffer->name);
+ e->new = str_from_cstr (new_name);
+}
+
+static void
+relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_remove *e = &m->data.buffer_remove;
+ e->event = RELAY_EVENT_BUFFER_REMOVE;
+ e->buffer_name = str_from_cstr (buffer->name);
+}
+
+static void
+relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_activate *e = &m->data.buffer_activate;
+ e->event = RELAY_EVENT_BUFFER_ACTIVATE;
+ e->buffer_name = str_from_cstr (buffer->name);
+}
+
+static void
+relay_prepare_buffer_clear (struct app_context *ctx,
+ struct buffer *buffer)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_buffer_clear *e = &m->data.buffer_clear;
+ e->event = RELAY_EVENT_BUFFER_CLEAR;
+ e->buffer_name = str_from_cstr (buffer->name);
+}
+
+enum relay_server_state
+relay_server_state_for_server (struct server *s)
+{
+ switch (s->state)
+ {
+ case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED;
+ case IRC_CONNECTING: return RELAY_SERVER_STATE_CONNECTING;
+ case IRC_CONNECTED: return RELAY_SERVER_STATE_CONNECTED;
+ case IRC_REGISTERED: return RELAY_SERVER_STATE_REGISTERED;
+ case IRC_CLOSING:
+ case IRC_HALF_CLOSED: return RELAY_SERVER_STATE_DISCONNECTING;
+ }
+ return 0;
+}
+
+static void
+relay_prepare_server_update (struct app_context *ctx, struct server *s)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_server_update *e = &m->data.server_update;
+ e->event = RELAY_EVENT_SERVER_UPDATE;
+ e->server_name = str_from_cstr (s->name);
+ e->data.state = relay_server_state_for_server (s);
+ if (s->state == IRC_REGISTERED)
+ {
+ char *user_utf8 = irc_to_utf8 (s->irc_user->nickname);
+ e->data.registered.user = str_from_cstr (user_utf8);
+ free (user_utf8);
+
+ char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str);
+ e->data.registered.user_modes = str_from_cstr (user_modes_utf8);
+ free (user_modes_utf8);
+ }
+}
+
+static void
+relay_prepare_server_rename (struct app_context *ctx, struct server *s,
+ const char *new_name)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_server_rename *e = &m->data.server_rename;
+ e->event = RELAY_EVENT_SERVER_RENAME;
+ e->server_name = str_from_cstr (s->name);
+ e->new = str_from_cstr (new_name);
+}
+
+static void
+relay_prepare_server_remove (struct app_context *ctx, struct server *s)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_server_remove *e = &m->data.server_remove;
+ e->event = RELAY_EVENT_SERVER_REMOVE;
+ e->server_name = str_from_cstr (s->name);
+}
+
+static void
+relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_error *e = &m->data.error;
+ e->event = RELAY_EVENT_ERROR;
+ e->command_seq = seq;
+ e->error = str_from_cstr (message);
+}
+
+static struct relay_event_data_response *
+relay_prepare_response (struct app_context *ctx, uint32_t seq)
+{
+ struct relay_event_message *m = relay_prepare (ctx);
+ struct relay_event_data_response *e = &m->data.response;
+ e->event = RELAY_EVENT_RESPONSE;
+ e->command_seq = seq;
+ return e;
+}
+
+// --- Terminal output ---------------------------------------------------------
+
+/// Default colour pair
+#define COLOR_DEFAULT -1
+
+/// Bright versions of the basic colour set
+#define COLOR_BRIGHT(x) (COLOR_ ## x + 8)
+
+/// Builds a colour pair for 256-colour terminals with a 16-colour backup value
+#define COLOR_256(name, c256) \
+ (((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16))
+
+typedef int (*terminal_printer_fn) (int);
+
+static int
+putchar_stderr (int c)
+{
+ return fputc (c, stderr);
+}
+
+static terminal_printer_fn
+get_attribute_printer (FILE *stream)
+{
+ if (stream == stdout && g_terminal.stdout_is_tty)
+ return putchar;
+ if (stream == stderr && g_terminal.stderr_is_tty)
+ return putchar_stderr;
+ return NULL;
+}
+
+// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+// A little tool that tries to make the most of the terminal's capabilities
+// to set up text attributes. It mostly targets just terminal emulators as that
+// is what people are using these days. At least no stupid ncurses limits us
+// with colour pairs.
+
+struct attr_printer
+{
+ struct attrs *attrs; ///< Named attributes
+ FILE *stream; ///< Output stream
+ bool dirty; ///< Attributes are set
+};
+
+#define ATTR_PRINTER_INIT(attrs, stream) { attrs, stream, true }
+
+static void
+attr_printer_filtered_puts (FILE *stream, const char *attr)
+{
+ for (; *attr; attr++)
+ {
+ // sgr/set_attributes and sgr0/exit_attribute_mode like to enable or
+ // disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R`
+ // does not skip over these characters and it screws up word wrapping
+ if (*attr == 14 /* SO */ || *attr == 15 /* SI */)
+ continue;
+
+ // Trivially skip delay sequences intended to be processed by tputs()
+ const char *end = NULL;
+ if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>')))
+ attr = end;
+ else
+ fputc (*attr, stream);
+ }
+}
+
+static void
+attr_printer_tputs (struct attr_printer *self, const char *attr)
+{
+ terminal_printer_fn printer = get_attribute_printer (self->stream);
+ if (printer)
+ tputs (attr, 1, printer);
+ else
+ // We shouldn't really do this but we need it to output formatting
+ // to the pager--it should be SGR-only
+ attr_printer_filtered_puts (self->stream, attr);
+}
+
+static void
+attr_printer_reset (struct attr_printer *self)
+{
+ if (self->dirty)
+ attr_printer_tputs (self, exit_attribute_mode);
+
+ self->dirty = false;
+}
+
+// NOTE: commonly terminals have:
+// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
+// 16 colours (okayish, we have the full basic range guaranteed)
+// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey)
+// 256 colours (best, like above but with a larger cube and more grey)
+
+/// Interpolate from the 256-colour palette to the 88-colour one
+static int
+attr_printer_256_to_88 (int color)
+{
+ // These colours are the same everywhere
+ if (color < 16)
+ return color;
+
+ // 24 -> 8 extra shades of grey
+ if (color >= 232)
+ return 80 + (color - 232) / 3;
+
+ // 6 * 6 * 6 cube -> 4 * 4 * 4 cube
+ int x[6] = { 0, 1, 1, 2, 2, 3 };
+ int index = color - 16;
+ return 16 +
+ ( x[ index / 36 ] << 8
+ | x[(index / 6) % 6 ] << 4
+ | x[(index % 6) ] );
+}
+
+static int
+attr_printer_decode_color (int color, bool *is_bright)
+{
+ int16_t c16 = color; hard_assert (c16 < 16);
+ int16_t c256 = color >> 16; hard_assert (c256 < 256);
+
+ *is_bright = false;
+ switch (max_colors)
+ {
+ case 8:
+ if (c16 >= 8)
+ {
+ c16 -= 8;
+ *is_bright = true;
+ }
+ // Fall-through
+ case 16:
+ return c16;
+
+ case 88:
+ return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256);
+ case 256:
+ return c256 <= 0 ? c16 : c256;
+
+ default:
+ // Unsupported palette
+ return -1;
+ }
+}
+
+static void
+attr_printer_apply (struct attr_printer *self,
+ int text_attrs, int wanted_fg, int wanted_bg)
+{
+ bool fg_is_bright;
+ int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright);
+ bool bg_is_bright;
+ int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright);
+
+ bool have_inverse = !!(text_attrs & TEXT_INVERSE);
+ if (have_inverse)
+ {
+ bool tmp = fg_is_bright;
+ fg_is_bright = bg_is_bright;
+ bg_is_bright = tmp;
+ }
+
+ // In 8 colour mode, some terminals don't support bright backgrounds.
+ // However, we can make use of the fact that the brightness change caused
+ // by the bold attribute is retained when inverting the colours.
+ // This has the downside of making the text bold when it's not supposed
+ // to be, and we still can't make both colours bright, so it's more of
+ // an interesting hack rather than anything else.
+ if (!fg_is_bright && bg_is_bright && have_inverse)
+ text_attrs |= TEXT_BOLD;
+ else if (!fg_is_bright && bg_is_bright
+ && !have_inverse && fg >= 0 && bg >= 0)
+ {
+ // As long as none of the colours is the default, we can swap them
+ int tmp = fg; fg = bg; bg = tmp;
+ text_attrs |= TEXT_BOLD | TEXT_INVERSE;
+ }
+ else
+ {
+ // This often works, however...
+ if (fg_is_bright) text_attrs |= TEXT_BOLD;
+ // this turns out to be annoying if implemented "correctly"
+ if (bg_is_bright) text_attrs |= TEXT_BLINK;
+ }
+
+ attr_printer_reset (self);
+
+ // TEXT_MONOSPACE is unimplemented, for obvious reasons
+ if (text_attrs)
+ attr_printer_tputs (self, tparm (set_attributes,
+ 0, // standout
+ text_attrs & TEXT_UNDERLINE,
+ text_attrs & TEXT_INVERSE,
+ text_attrs & TEXT_BLINK,
+ 0, // dim
+ text_attrs & TEXT_BOLD,
+ 0, // blank
+ 0, // protect
+ 0)); // acs
+ if ((text_attrs & TEXT_ITALIC) && enter_italics_mode)
+ attr_printer_tputs (self, enter_italics_mode);
+
+ char *smxx = NULL;
+ if ((text_attrs & TEXT_CROSSED_OUT)
+ && (smxx = tigetstr ("smxx")) && smxx != (char *) -1)
+ attr_printer_tputs (self, smxx);
+
+ if (fg >= 0)
+ attr_printer_tputs (self, g_terminal.color_set_fg[fg]);
+ if (bg >= 0)
+ attr_printer_tputs (self, g_terminal.color_set_bg[bg]);
+
+ self->dirty = true;
+}
+
+static void
+attr_printer_apply_named (struct attr_printer *self, int attribute)
+{
+ attr_printer_reset (self);
+ if (attribute == ATTR_RESET)
+ return;
+
+ // See the COLOR_256 macro or attr_printer_decode_color().
+ struct attrs *a = &self->attrs[attribute];
+ attr_printer_apply (self, a->attrs,
+ a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)),
+ a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF)));
+ self->dirty = true;
+}
+
+// ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+static void
+vprint_attributed (struct app_context *ctx,
+ FILE *stream, intptr_t attribute, const char *fmt, va_list ap)
+{
+ terminal_printer_fn printer = get_attribute_printer (stream);
+ if (!attribute)
+ printer = NULL;
+
+ struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream);
+ if (printer)
+ attr_printer_apply_named (&state, attribute);
+
+ vfprintf (stream, fmt, ap);
+
+ if (printer)
+ attr_printer_reset (&state);
+}
+
+static void
+print_attributed (struct app_context *ctx,
+ FILE *stream, intptr_t attribute, const char *fmt, ...)
+{
+ va_list ap;
+ va_start (ap, fmt);
+ vprint_attributed (ctx, stream, attribute, fmt, ap);
+ va_end (ap);
+}
+
+static void
+log_message_attributed (void *user_data, const char *quote, const char *fmt,
+ va_list ap)
+{
+ FILE *stream = stderr;
+ struct app_context *ctx = g_ctx;
+
+ CALL (ctx->input, hide);
+
+ print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
+ vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
+ fputs ("\n", stream);
+
+ CALL (ctx->input, show);
+}
+
+// ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+static ssize_t
+attr_by_name (const char *name)
+{
+ static const char *table[ATTR_COUNT] =
+ {
+ NULL,
+#define XX(x, y, z) [ATTR_ ## x] = #y,
+ ATTR_TABLE (XX)
+#undef XX
+ };
+
+ for (size_t i = 1; i < N_ELEMENTS (table); i++)
+ if (!strcmp (name, table[i]))
+ return i;
+ return -1;
+}
+
+static void
+on_config_theme_change (struct config_item *item)
+{
+ struct app_context *ctx = item->user_data;
+ ssize_t id = attr_by_name (item->schema->name);
+ if (id != -1)
+ {
+ // TODO: There should be a validator.
+ ctx->theme[id] = item->type == CONFIG_ITEM_NULL
+ ? ctx->theme_defaults[id]
+ : attrs_decode (item->value.string.str);
+ }
+}
+
+static void
+init_colors (struct app_context *ctx)
+{
+ bool have_ti = init_terminal ();
+
+#define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \
+ ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ }
+
+ INIT_ATTR (PROMPT, -1, -1, TEXT_BOLD);
+ INIT_ATTR (RESET, -1, -1, 0);
+ INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD);
+ INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1, 0);
+ INIT_ATTR (WARNING, COLOR_YELLOW, -1, 0);
+ INIT_ATTR (ERROR, COLOR_RED, -1, 0);
+
+ INIT_ATTR (EXTERNAL, COLOR_WHITE, -1, 0);
+ INIT_ATTR (TIMESTAMP, COLOR_WHITE, -1, 0);
+ INIT_ATTR (HIGHLIGHT, COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD);
+ INIT_ATTR (ACTION, COLOR_RED, -1, 0);
+ INIT_ATTR (USERHOST, COLOR_CYAN, -1, 0);
+ INIT_ATTR (JOIN, COLOR_GREEN, -1, 0);
+ INIT_ATTR (PART, COLOR_RED, -1, 0);
+
+#undef INIT_ATTR
+
+ // This prevents formatters from obtaining an attribute printer function
+ if (!have_ti)
+ {
+ g_terminal.stdout_is_tty = false;
+ g_terminal.stderr_is_tty = false;
+ }
+
+ g_log_message_real = log_message_attributed;
+}
+
+// --- Helpers -----------------------------------------------------------------
+
+static int
+irc_server_strcmp (struct server *s, const char *a, const char *b)
+{
+ int x;
+ while (*a || *b)
+ if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
+ return x;
+ return 0;
+}
+
+static int
+irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n)
+{
+ int x;
+ while (n-- && (*a || *b))
+ if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
+ return x;
+ return 0;
+}
+
+static char *
+irc_cut_nickname (const char *prefix)
+{
+ return cstr_cut_until (prefix, "!@");
+}
+
+static const char *
+irc_find_userhost (const char *prefix)
+{
+ const char *p = strchr (prefix, '!');
+ return p ? p + 1 : NULL;
+}
+
+static bool
+irc_is_this_us (struct server *s, const char *prefix)
+{
+ // This shouldn't be called before successfully registering.
+ // Better safe than sorry, though.
+ if (!s->irc_user)
+ return false;
+
+ char *nick = irc_cut_nickname (prefix);
+ bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname);
+ free (nick);
+ return result;
+}
+
+static bool
+irc_is_channel (struct server *s, const char *ident)
+{
+ return *ident
+ && (!!strchr (s->irc_chantypes, *ident) ||
+ !!strchr (s->irc_idchan_prefixes, *ident));
+}
+
+// Message targets can be prefixed by a character filtering their targets
+static const char *
+irc_skip_statusmsg (struct server *s, const char *target)
+{
+ return target + (*target && strchr (s->irc_statusmsg, *target));
+}
+
+static bool
+irc_is_extban (struct server *s, const char *target)
+{
+ // Some servers have a prefix, and some support negation
+ if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix)
+ return false;
+ if (*target == '~')
+ target++;
+
+ // XXX: we don't know if it's supposed to have an argument, or not
+ return *target && strchr (s->irc_extban_types, *target++)
+ && strchr (":\0", *target);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it
+// as ISO Latin 1. This function should not be called on the whole message.
+static char *
+irc_to_utf8 (const char *text)
+{
+ if (!text)
+ return NULL;
+
+ // XXX: the validation may be unnecessarily harsh, could do with a lenient
+ // first pass, then replace any errors with the replacement character
+ size_t len = strlen (text) + 1;
+ if (utf8_validate (text, len))
+ return xstrdup (text);
+
+ // Windows 1252 redefines several silly C1 control characters as glyphs
+ static const char c1[32][4] =
+ {
+ "\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92",
+ "\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1",
+ "\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9",
+ "\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f",
+ "\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c",
+ "\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94",
+ "\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba",
+ "\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8",
+ };
+
+ struct str s = str_make ();
+ for (const char *p = text; *p; p++)
+ {
+ int c = *(unsigned char *) p;
+ if (c < 0x80)
+ str_append_c (&s, c);
+ else if (c < 0xA0)
+ str_append (&s, c1[c & 0x1f]);
+ else
+ str_append_data (&s,
+ (char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2);
+ }
+ return str_steal (&s);
+}
+
+// --- Output formatter --------------------------------------------------------
+
+// This complicated piece of code makes attributed text formatting simple.
+// We use a printf-inspired syntax to push attributes and text to the object,
+// then flush it either to a terminal, or a log file with formatting stripped.
+//
+// Format strings use a #-quoted notation, to differentiate from printf:
+// #s inserts a string (expected to be in UTF-8)
+// #d inserts a signed integer
+// #l inserts a locale-encoded string
+//
+// #S inserts a string from the server in an unknown encoding
+// #m inserts an IRC-formatted string (auto-resets at boundaries)
+// #n cuts the nickname from a string and automatically colours it
+// #N is like #n but also appends userhost, if present
+//
+// #a inserts named attributes (auto-resets)
+// #r resets terminal attributes
+// #c sets foreground colour
+// #C sets background colour
+//
+// Modifiers:
+// & free() the string argument after using it
+
+static void
+formatter_add_item (struct formatter *self, struct formatter_item template_)
+{
+ // Auto-resetting tends to create unnecessary items,
+ // which also end up being relayed to frontends, so filter them out.
+ bool reset =
+ template_.type == FORMATTER_ITEM_ATTR &&
+ template_.attribute == ATTR_RESET;
+ if (self->clean && reset)
+ return;
+
+ self->clean = reset ||
+ (self->clean && template_.type == FORMATTER_ITEM_TEXT);
+
+ if (template_.text)
+ template_.text = xstrdup (template_.text);
+
+ if (self->items_len == self->items_alloc)
+ self->items = xreallocarray
+ (self->items, sizeof *self->items, (self->items_alloc <<= 1));
+ self->items[self->items_len++] = template_;
+}
+
+#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
+ (struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
+
+#define FORMATTER_ADD_RESET(self) \
+ FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
+#define FORMATTER_ADD_TEXT(self, text_) \
+ FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum
+{
+ MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN,
+ MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE,
+ MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN,
+ MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY,
+};
+
+// We use estimates from the 16 colour terminal palette, or the 256 colour cube,
+// which is not always available. The mIRC orange colour is only in the cube.
+
+static const int g_mirc_to_terminal[] =
+{
+ [MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231),
+ [MIRC_BLACK] = COLOR_256 (BLACK, 16),
+ [MIRC_BLUE] = COLOR_256 (BLUE, 19),
+ [MIRC_GREEN] = COLOR_256 (GREEN, 34),
+ [MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196),
+ [MIRC_RED] = COLOR_256 (RED, 124),
+ [MIRC_PURPLE] = COLOR_256 (MAGENTA, 127),
+ [MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214),
+ [MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226),
+ [MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46),
+ [MIRC_CYAN] = COLOR_256 (CYAN, 37),
+ [MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51),
+ [MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21),
+ [MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201),
+ [MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244),
+ [MIRC_L_GRAY] = COLOR_256 (WHITE, 252),
+};
+
+// https://modern.ircdocs.horse/formatting.html
+// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html
+static const int16_t g_extra_to_256[100 - 16] =
+{
+ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
+ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
+ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
+ 196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198,
+ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
+ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
+ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1
+};
+
+static const char *
+irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg)
+{
+ if (!isdigit_ascii (*s))
+ {
+ *fg = *bg = 99;
+ return s;
+ }
+
+ *fg = *s++ - '0';
+ if (isdigit_ascii (*s))
+ *fg = *fg * 10 + (*s++ - '0');
+
+ if (*s != ',' || !isdigit_ascii (s[1]))
+ return s;
+ s++;
+
+ *bg = *s++ - '0';
+ if (isdigit_ascii (*s))
+ *bg = *bg * 10 + (*s++ - '0');
+ return s;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct irc_char_attrs
+{
+ uint8_t fg, bg; ///< {Fore,back}ground colour or 99
+ uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK
+ uint8_t starts_at_boundary; ///< Possible to split here?
+};
+
+static void
+irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out)
+{
+ soft_assert (attrs->fg < 100 && attrs->bg < 100);
+
+ if (attrs->fg != 99 || attrs->bg != 99)
+ {
+ str_append_printf (out, "\x03%u", attrs->fg);
+ if (attrs->bg != 99)
+ str_append_printf (out, ",%02u", attrs->bg);
+ }
+ if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02');
+ if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d');
+ if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f');
+ if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16');
+ if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e');
+ if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11');
+}
+
+static int
+irc_parse_attribute (char c)
+{
+ switch (c)
+ {
+ case '\x02' /* ^B */: return TEXT_BOLD;
+ case '\x11' /* ^Q */: return TEXT_MONOSPACE;
+ case '\x16' /* ^V */: return TEXT_INVERSE;
+ case '\x1d' /* ^] */: return TEXT_ITALIC;
+ case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT;
+ case '\x1f' /* ^_ */: return TEXT_UNDERLINE;
+ case '\x0f' /* ^O */: return -1;
+ }
+ return 0;
+}
+
+// The text needs to be NUL-terminated, and a valid UTF-8 string
+static struct irc_char_attrs *
+irc_analyze_text (const char *text, size_t len)
+{
+ struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs),
+ blank = { .fg = 99, .bg = 99, .starts_at_boundary = true },
+ next = blank, cur = next;
+
+ for (size_t i = 0; i != len; cur = next)
+ {
+ const char *start = text;
+ hard_assert (utf8_decode (&text, len - i) >= 0);
+
+ int attribute = irc_parse_attribute (*start);
+ if (*start == '\x03')
+ text = irc_parse_mirc_color (text, &next.fg, &next.bg);
+ else if (attribute > 0)
+ next.attributes ^= attribute;
+ else if (attribute < 0)
+ next = blank;
+
+ while (start++ != text)
+ {
+ attrs[i++] = cur;
+ cur.starts_at_boundary = false;
+ }
+ }
+ return attrs;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *
+formatter_parse_mirc_color (struct formatter *self, const char *s)
+{
+ uint8_t fg = 255, bg = 255;
+ s = irc_parse_mirc_color (s, &fg, &bg);
+
+ if (fg < 16)
+ FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
+ else if (fg < 100)
+ FORMATTER_ADD_ITEM (self, FG_COLOR,
+ .color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16]));
+
+ if (bg < 16)
+ FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
+ else if (bg < 100)
+ FORMATTER_ADD_ITEM (self, BG_COLOR,
+ .color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16]));
+
+ return s;
+}
+
+static void
+formatter_parse_message (struct formatter *self, const char *s)
+{
+ FORMATTER_ADD_RESET (self);
+
+ struct str buf = str_make ();
+ unsigned char c;
+ while ((c = *s++))
+ {
+ if (buf.len && c < 0x20)
+ {
+ FORMATTER_ADD_TEXT (self, buf.str);
+ str_reset (&buf);
+ }
+
+ int attribute = irc_parse_attribute (c);
+ if (c == '\x03')
+ s = formatter_parse_mirc_color (self, s);
+ else if (attribute > 0)
+ FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute);
+ else if (attribute < 0)
+ FORMATTER_ADD_RESET (self);
+ else
+ str_append_c (&buf, c);
+ }
+
+ if (buf.len)
+ FORMATTER_ADD_TEXT (self, buf.str);
+
+ str_free (&buf);
+ FORMATTER_ADD_RESET (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+formatter_parse_nick (struct formatter *self, const char *s)
+{
+ // For outgoing messages; maybe we should add a special #t for them
+ // which would also make us not cut off the userhost part, ever
+ if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s)))
+ {
+ char *tmp = irc_to_utf8 (s);
+ FORMATTER_ADD_TEXT (self, tmp);
+ free (tmp);
+ return;
+ }
+
+ char *nick = irc_cut_nickname (s);
+ int color = siphash_wrapper (nick, strlen (nick)) % 7;
+
+ // Never use the black colour, could become transparent on black terminals;
+ // white is similarly excluded from the range
+ if (color == COLOR_BLACK)
+ color = (uint16_t) -1;
+
+ // Use a colour from the 256-colour cube if available
+ color |= self->ctx->nick_palette[siphash_wrapper (nick,
+ strlen (nick)) % self->ctx->nick_palette_len] << 16;
+
+ // We always use the default colour for ourselves
+ if (self->s && irc_is_this_us (self->s, nick))
+ color = -1;
+
+ FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
+
+ char *x = irc_to_utf8 (nick);
+ free (nick);
+ FORMATTER_ADD_TEXT (self, x);
+ free (x);
+
+ // Need to reset the colour afterwards
+ FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
+}
+
+static void
+formatter_parse_nick_full (struct formatter *self, const char *s)
+{
+ formatter_parse_nick (self, s);
+
+ const char *userhost;
+ if (!(userhost = irc_find_userhost (s)))
+ return;
+
+ FORMATTER_ADD_TEXT (self, " (");
+ FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);
+
+ char *x = irc_to_utf8 (userhost);
+ FORMATTER_ADD_TEXT (self, x);
+ free (x);
+
+ FORMATTER_ADD_RESET (self);
+ FORMATTER_ADD_TEXT (self, ")");
+}
+
+static const char *
+formatter_parse_field (struct formatter *self,
+ const char *field, struct str *buf, va_list *ap)
+{
+ bool free_string = false;
+ char *s = NULL;
+ char *tmp = NULL;
+ int c;
+
+restart:
+ switch ((c = *field++))
+ {
+ // We can push boring text content to the caller's buffer
+ // and let it flush the buffer only when it's actually needed
+ case 'd':
+ tmp = xstrdup_printf ("%d", va_arg (*ap, int));
+ str_append (buf, tmp);
+ free (tmp);
+ break;
+ case 's':
+ str_append (buf, (s = va_arg (*ap, char *)));
+ break;
+ case 'l':
+ if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
+ (s = va_arg (*ap, char *)), -1, NULL)))
+ print_error ("character conversion failed for: %s", "output");
+ else
+ str_append (buf, tmp);
+ free (tmp);
+ break;
+
+ case 'S':
+ tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
+ str_append (buf, tmp);
+ free (tmp);
+ break;
+ case 'm':
+ tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
+ formatter_parse_message (self, tmp);
+ free (tmp);
+ break;
+ case 'n':
+ formatter_parse_nick (self, (s = va_arg (*ap, char *)));
+ break;
+ case 'N':
+ formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
+ break;
+
+ case 'a':
+ FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
+ break;
+ case 'c':
+ FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
+ break;
+ case 'C':
+ FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
+ break;
+ case 'r':
+ FORMATTER_ADD_RESET (self);
+ break;
+
+ default:
+ if (c == '&' && !free_string)
+ free_string = true;
+ else if (c)
+ hard_assert (!"unexpected format specifier");
+ else
+ hard_assert (!"unexpected end of format string");
+ goto restart;
+ }
+
+ if (free_string)
+ free (s);
+ return field;
+}
+
+// I was unable to take a pointer of a bare "va_list" when it was passed in
+// as a function argument, so it has to be a pointer from the beginning
+static void
+formatter_addv (struct formatter *self, const char *format, va_list *ap)
+{
+ struct str buf = str_make ();
+ while (*format)
+ {
+ if (*format != '#' || *++format == '#')
+ {
+ str_append_c (&buf, *format++);
+ continue;
+ }
+ if (buf.len)
+ {
+ FORMATTER_ADD_TEXT (self, buf.str);
+ str_reset (&buf);
+ }
+
+ format = formatter_parse_field (self, format, &buf, ap);
+ }
+
+ if (buf.len)
+ FORMATTER_ADD_TEXT (self, buf.str);
+
+ str_free (&buf);
+}
+
+static void
+formatter_add (struct formatter *self, const char *format, ...)
+{
+ va_list ap;
+ va_start (ap, format);
+ formatter_addv (self, format, &ap);
+ va_end (ap);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct line_char_attrs
+{
+ short named; ///< Named attribute or -1
+ short text; ///< Text attributes
+ int fg; ///< Foreground colour (-1 for default)
+ int bg; ///< Background colour (-1 for default)
+};
+
+// We can get rid of the linked list and do this in one allocation (use strlen()
+// for the upper bound)--since we only prepend and/or replace characters, add
+// a member to specify the prepended character and how many times to repeat it.
+// Tabs may nullify the wide character but it's not necessary.
+//
+// This would be slighly more optimal but it would also set the algorithm in
+// stone and complicate flushing.
+
+struct line_char
+{
+ LIST_HEADER (struct line_char)
+
+ wchar_t wide; ///< The character as a wchar_t
+ int width; ///< Width of the character in cells
+ struct line_char_attrs attrs; ///< Attributes
+};
+
+static struct line_char *
+line_char_new (wchar_t wc)
+{
+ struct line_char *self = xcalloc (1, sizeof *self);
+ self->width = wcwidth ((self->wide = wc));
+
+ // Typically various control characters
+ if (self->width < 0)
+ self->width = 0;
+
+ self->attrs.bg = self->attrs.fg = -1;
+ self->attrs.named = ATTR_RESET;
+ return self;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct line_wrap_mark
+{
+ struct line_char *start; ///< First character
+ int used; ///< Display cells used
+};
+
+static void
+line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c)
+{
+ if (!mark->start)
+ mark->start = c;
+ mark->used += c->width;
+}
+
+struct line_wrap_state
+{
+ struct line_char *result; ///< Head of result
+ struct line_char *result_tail; ///< Tail of result
+
+ int line_used; ///< Line length before marks
+ int line_max; ///< Maximum line length
+ struct line_wrap_mark chunk; ///< All buffered text
+ struct line_wrap_mark overflow; ///< Overflowing text
+};
+
+static void
+line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before)
+{
+ struct line_char *nl = line_char_new (L'\n');
+ LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start);
+ s->line_used = before->used;
+}
+
+static void
+line_wrap_flush (struct line_wrap_state *s, bool force_split)
+{
+ if (!s->overflow.start)
+ s->line_used += s->chunk.used;
+ else if (force_split || s->chunk.used > s->line_max)
+ {
+#ifdef WRAP_UNNECESSARILY
+ // When the line wraps at the end of the screen and a background colour
+ // is set, the terminal paints the entire new line with that colour.
+ // Explicitly inserting a newline with the default attributes fixes it.
+ line_wrap_flush_split (s, &s->overflow);
+#else
+ // Splitting here breaks link searching mechanisms in some terminals,
+ // though, so we make a trade-off and let the chunk wrap naturally.
+ // Fuck terminals, really.
+ s->line_used = s->overflow.used;
+#endif
+ }
+ else
+ // Print the chunk in its entirety on a new line
+ line_wrap_flush_split (s, &s->chunk);
+
+ memset (&s->chunk, 0, sizeof s->chunk);
+ memset (&s->overflow, 0, sizeof s->overflow);
+}
+
+static void
+line_wrap_nl (struct line_wrap_state *s)
+{
+ line_wrap_flush (s, true);
+ struct line_char *nl = line_char_new (L'\n');
+ LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl);
+ s->line_used = 0;
+}
+
+static void
+line_wrap_tab (struct line_wrap_state *s, struct line_char *c)
+{
+ line_wrap_flush (s, true);
+ if (s->line_used >= s->line_max)
+ line_wrap_nl (s);
+
+ // Compute the number of characters needed to get to the next tab stop
+ int tab_width = ((s->line_used + 8) & ~7) - s->line_used;
+ // On overflow just fill the rest of the line with spaces
+ if (s->line_used + tab_width > s->line_max)
+ tab_width = s->line_max - s->line_used;
+
+ s->line_used += tab_width;
+ while (tab_width--)
+ {
+ struct line_char *space = line_char_new (L' ');
+ space->attrs = c->attrs;
+ LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space);
+ }
+}
+
+static void
+line_wrap_push_char (struct line_wrap_state *s, struct line_char *c)
+{
+ // Note that when processing whitespace here, any non-WS chunk has already
+ // been flushed, and thus it matters little if we flush with force split
+ if (wcschr (L"\r\f\v", c->wide))
+ /* Skip problematic characters */;
+ else if (c->wide == L'\n')
+ line_wrap_nl (s);
+ else if (c->wide == L'\t')
+ line_wrap_tab (s, c);
+ else
+ goto use_as_is;
+ free (c);
+ return;
+
+use_as_is:
+ if (s->overflow.start
+ || s->line_used + s->chunk.used + c->width > s->line_max)
+ {
+ if (s->overflow.used + c->width > s->line_max)
+ {
+#ifdef WRAP_UNNECESSARILY
+ // If the overflow overflows, restart on a new line
+ line_wrap_nl (s);
+#else
+ // See line_wrap_flush(), we would end up on a new line anyway
+ line_wrap_flush (s, true);
+ s->line_used = 0;
+#endif
+ }
+ else
+ line_wrap_mark_push (&s->overflow, c);
+ }
+ line_wrap_mark_push (&s->chunk, c);
+ LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c);
+}
+
+/// Basic word wrapping that respects wcwidth(3) and expands tabs.
+/// Besides making text easier to read, it also fixes the problem with
+/// formatting spilling over the entire new line on line wrap.
+static struct line_char *
+line_wrap (struct line_char *line, int max_width)
+{
+ struct line_wrap_state s = { .line_max = max_width };
+ bool last_was_word_char = false;
+ LIST_FOR_EACH (struct line_char, c, line)
+ {
+ // Act on the right boundary of (\s*\S+) chunks
+ bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide);
+ if (last_was_word_char && !this_is_word_char)
+ line_wrap_flush (&s, false);
+ last_was_word_char = this_is_word_char;
+
+ LIST_UNLINK (line, c);
+ line_wrap_push_char (&s, c);
+ }
+
+ // Make sure to process the last word and return the modified list
+ line_wrap_flush (&s, false);
+ return s.result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct exploder
+{
+ struct app_context *ctx; ///< Application context
+ struct line_char *result; ///< Result
+ struct line_char *result_tail; ///< Tail of result
+ struct line_char_attrs attrs; ///< Current attributes
+};
+
+static bool
+explode_formatter_attr (struct exploder *self, struct formatter_item *item)
+{
+ switch (item->type)
+ {
+ case FORMATTER_ITEM_ATTR:
+ self->attrs.named = item->attribute;
+ self->attrs.text = 0;
+ self->attrs.fg = -1;
+ self->attrs.bg = -1;
+ return true;
+ case FORMATTER_ITEM_SIMPLE:
+ self->attrs.named = -1;
+ self->attrs.text ^= item->attribute;
+ return true;
+ case FORMATTER_ITEM_FG_COLOR:
+ self->attrs.named = -1;
+ self->attrs.fg = item->color;
+ return true;
+ case FORMATTER_ITEM_BG_COLOR:
+ self->attrs.named = -1;
+ self->attrs.bg = item->color;
+ return true;
+ default:
+ return false;
+ }
+}
+
+static void
+explode_text (struct exploder *self, const char *text)
+{
+ // Throw away any potentially harmful control characters first
+ struct str filtered = str_make ();
+ for (const char *p = text; *p; p++)
+ if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p))
+ str_append_c (&filtered, *p);
+
+ size_t term_len = 0, processed = 0, len;
+ char *term = iconv_xstrdup (self->ctx->term_from_utf8,
+ filtered.str, filtered.len + 1, &term_len);
+ str_free (&filtered);
+
+ mbstate_t ps;
+ memset (&ps, 0, sizeof ps);
+
+ wchar_t wch;
+ while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
+ {
+ hard_assert (len != (size_t) -2 && len != (size_t) -1);
+ hard_assert ((processed += len) <= term_len);
+
+ struct line_char *c = line_char_new (wch);
+ c->attrs = self->attrs;
+ LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c);
+ }
+ free (term);
+}
+
+static struct line_char *
+formatter_to_chars (struct formatter *formatter)
+{
+ struct exploder self = { .ctx = formatter->ctx };
+ self.attrs.fg = self.attrs.bg = self.attrs.named = -1;
+
+ int attribute_ignore = 0;
+ for (size_t i = 0; i < formatter->items_len; i++)
+ {
+ struct formatter_item *iter = &formatter->items[i];
+ if (iter->type == FORMATTER_ITEM_TEXT)
+ explode_text (&self, iter->text);
+ else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR)
+ attribute_ignore += iter->attribute;
+ else if (attribute_ignore <= 0
+ && !explode_formatter_attr (&self, iter))
+ hard_assert (!"unhandled formatter item type");
+ }
+ return self.result;
+}
+
+enum
+{
+ FLUSH_OPT_RAW = (1 << 0), ///< Print raw attributes
+ FLUSH_OPT_NOWRAP = (1 << 1) ///< Do not wrap
+};
+
+/// The input is a bunch of wide characters--respect shift state encodings
+static void
+formatter_putc (struct line_char *c, FILE *stream)
+{
+ static mbstate_t mb;
+ char buf[MB_LEN_MAX] = {};
+ size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb);
+ if (len != (size_t) -1 && len)
+ fwrite (buf, len - !c, 1, stream);
+ free (c);
+}
+
+static void
+formatter_flush (struct formatter *self, FILE *stream, int flush_opts)
+{
+ struct line_char *line = formatter_to_chars (self);
+
+ bool is_tty = !!get_attribute_printer (stream);
+ if (!is_tty && !(flush_opts & FLUSH_OPT_RAW))
+ {
+ LIST_FOR_EACH (struct line_char, c, line)
+ formatter_putc (c, stream);
+ formatter_putc (NULL, stream);
+ return;
+ }
+
+ if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP))
+ line = line_wrap (line, g_terminal.columns);
+
+ struct attr_printer state = ATTR_PRINTER_INIT (self->ctx->theme, stream);
+ struct line_char_attrs attrs = {}; // Won't compare equal to anything
+ LIST_FOR_EACH (struct line_char, c, line)
+ {
+ if (attrs.fg != c->attrs.fg
+ || attrs.bg != c->attrs.bg
+ || attrs.named != c->attrs.named
+ || attrs.text != c->attrs.text)
+ {
+ formatter_putc (NULL, stream);
+
+ attrs = c->attrs;
+ if (attrs.named == -1)
+ attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg);
+ else
+ attr_printer_apply_named (&state, attrs.named);
+ }
+
+ formatter_putc (c, stream);
+ }
+ formatter_putc (NULL, stream);
+ attr_printer_reset (&state);
+}
+
+// --- Buffers -----------------------------------------------------------------
+
+static void
+buffer_pop_excess_lines (struct app_context *ctx, struct buffer *self)
+{
+ int to_delete = (int) self->lines_count - (int) ctx->backlog_limit;
+ while (to_delete-- > 0 && self->lines)
+ {
+ struct buffer_line *excess = self->lines;
+ LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, excess);
+ buffer_line_destroy (excess);
+ self->lines_count--;
+ }
+}
+
+static void
+on_config_backlog_limit_change (struct config_item *item)
+{
+ struct app_context *ctx = item->user_data;
+ ctx->backlog_limit = MIN (item->value.integer, INT_MAX);
+
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ buffer_pop_excess_lines (ctx, iter);
+}
+
+static void
+buffer_update_time (struct app_context *ctx, time_t now, FILE *stream,
+ int flush_opts)
+{
+ struct tm last, current;
+ if (!localtime_r (&ctx->last_displayed_msg_time, &last)
+ || !localtime_r (&now, &current))
+ {
+ // Strange but nonfatal
+ print_error ("%s: %s", "localtime_r", strerror (errno));
+ return;
+ }
+
+ ctx->last_displayed_msg_time = now;
+ if (last.tm_year == current.tm_year
+ && last.tm_mon == current.tm_mon
+ && last.tm_mday == current.tm_mday)
+ return;
+
+ char buf[64] = "";
+ const char *format =
+ get_config_string (ctx->config.root, "general.date_change_line");
+ if (!strftime (buf, sizeof buf, format, &current))
+ {
+ print_error ("%s: %s", "strftime", strerror (errno));
+ return;
+ }
+
+ struct formatter f = formatter_make (ctx, NULL);
+ formatter_add (&f, "#a#s\n", ATTR_DATE_CHANGE, buf);
+ formatter_flush (&f, stream, flush_opts);
+ // Flush the trailing formatting reset item
+ fflush (stream);
+ formatter_free (&f);
+}
+
+static void
+buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output,
+ int flush_opts)
+{
+ switch (line->r)
+ {
+ case BUFFER_LINE_BARE: break;
+ case BUFFER_LINE_INDENT: formatter_add (f, " "); break;
+ case BUFFER_LINE_STATUS: formatter_add (f, " - "); break;
+ case BUFFER_LINE_ERROR: formatter_add (f, "#a=!=#r ", ATTR_ERROR); break;
+ case BUFFER_LINE_JOIN: formatter_add (f, "#a-->#r ", ATTR_JOIN); break;
+ case BUFFER_LINE_PART: formatter_add (f, "#a<--#r ", ATTR_PART); break;
+ case BUFFER_LINE_ACTION: formatter_add (f, " #a*#r ", ATTR_ACTION); break;
+ }
+
+ for (struct formatter_item *iter = line->items; iter->type; iter++)
+ formatter_add_item (f, *iter);
+
+ formatter_add (f, "\n");
+ formatter_flush (f, output, flush_opts);
+ formatter_free (f);
+}
+
+static void
+buffer_line_write_time (struct formatter *f, struct buffer_line *line,
+ FILE *stream, int flush_opts)
+{
+ // Normal timestamps don't include the date, make sure the user won't be
+ // confused as to when an event has happened
+ buffer_update_time (f->ctx, line->when, stream, flush_opts);
+
+ struct tm current;
+ char buf[9];
+ if (!localtime_r (&line->when, &current))
+ print_error ("%s: %s", "localtime_r", strerror (errno));
+ else if (!strftime (buf, sizeof buf, "%T", &current))
+ print_error ("%s: %s", "strftime", "buffer too small");
+ else
+ formatter_add (f, "#a#s#r ", ATTR_TIMESTAMP, buf);
+}
+
+#define buffer_line_will_show_up(buffer, line) \
+ (!(buffer)->hide_unimportant || !((line)->flags & BUFFER_LINE_UNIMPORTANT))
+
+static void
+buffer_line_display (struct app_context *ctx,
+ struct buffer *buffer, struct buffer_line *line, bool is_external)
+{
+ if (!buffer_line_will_show_up (buffer, line))
+ return;
+
+ CALL (ctx->input, hide);
+
+ struct formatter f = formatter_make (ctx, NULL);
+ buffer_line_write_time (&f, line, stdout, 0);
+
+ // Ignore all formatting for messages coming from other buffers, that is
+ // either from the global or server buffer. Instead print them in grey.
+ if (is_external)
+ {
+ formatter_add (&f, "#a", ATTR_EXTERNAL);
+ FORMATTER_ADD_ITEM (&f, IGNORE_ATTR, .attribute = 1);
+ }
+ buffer_line_flush (line, &f, stdout, 0);
+ // Flush the trailing formatting reset item
+ fflush (stdout);
+
+ CALL (ctx->input, show);
+}
+
+static void
+buffer_line_write_to_backlog (struct app_context *ctx,
+ struct buffer_line *line, FILE *log_file, int flush_opts)
+{
+ struct formatter f = formatter_make (ctx, NULL);
+ buffer_line_write_time (&f, line, log_file, flush_opts);
+ buffer_line_flush (line, &f, log_file, flush_opts);
+}
+
+static void
+buffer_line_write_to_log (struct app_context *ctx,
+ struct buffer_line *line, FILE *log_file)
+{
+ if (line->flags & BUFFER_LINE_SKIP_FILE)
+ return;
+
+ struct formatter f = formatter_make (ctx, NULL);
+ struct tm current;
+ char buf[20];
+ if (!gmtime_r (&line->when, &current))
+ print_error ("%s: %s", "gmtime_r", strerror (errno));
+ else if (!strftime (buf, sizeof buf, "%F %T", &current))
+ print_error ("%s: %s", "strftime", "buffer too small");
+ else
+ formatter_add (&f, "#s ", buf);
+
+ // The target is not a terminal, thus it won't wrap in spite of the 0
+ buffer_line_flush (line, &f, log_file, 0);
+}
+
+static void
+log_formatter (struct app_context *ctx, struct buffer *buffer,
+ unsigned flags, enum buffer_line_rendition r, struct formatter *f)
+{
+ if (!buffer)
+ buffer = ctx->global_buffer;
+
+ struct buffer_line *line = buffer_line_new (f);
+ line->flags = flags;
+ line->r = r;
+ // TODO: allow providing custom time (IRCv3.2 server-time)
+ line->when = time (NULL);
+
+ buffer_pop_excess_lines (ctx, buffer);
+ LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
+ buffer->lines_count++;
+
+ if (buffer->log_file)
+ buffer_line_write_to_log (ctx, line, buffer->log_file);
+
+ bool unseen_pm = buffer->type == BUFFER_PM
+ && buffer != ctx->current_buffer
+ && !(flags & BUFFER_LINE_UNIMPORTANT);
+ bool important = (flags & BUFFER_LINE_HIGHLIGHT) || unseen_pm;
+ if (ctx->beep_on_highlight && important)
+ // XXX: this may disturb any other foreground process
+ CALL (ctx->input, ding);
+
+ bool can_leak = false;
+ if ((buffer == ctx->global_buffer)
+ || (ctx->current_buffer->type == BUFFER_GLOBAL
+ && buffer->type == BUFFER_SERVER)
+ || (ctx->current_buffer->type != BUFFER_GLOBAL
+ && buffer == ctx->current_buffer->server->buffer))
+ can_leak = true;
+
+ relay_prepare_buffer_line (ctx, buffer, line,
+ buffer != ctx->current_buffer && !ctx->isolate_buffers && can_leak);
+ relay_broadcast (ctx);
+
+ bool displayed = true;
+ if (ctx->terminal_suspended > 0)
+ // Another process is using the terminal
+ displayed = false;
+ else if (buffer == ctx->current_buffer)
+ buffer_line_display (ctx, buffer, line, false);
+ else if (!ctx->isolate_buffers && can_leak)
+ buffer_line_display (ctx, buffer, line, true);
+ else
+ displayed = false;
+
+ // Advance the unread marker in active buffers but don't create a new one
+ if (!displayed
+ || (buffer == ctx->current_buffer && buffer->new_messages_count))
+ {
+ buffer->new_messages_count++;
+ if (flags & BUFFER_LINE_UNIMPORTANT)
+ buffer->new_unimportant_count++;
+ buffer->highlighted |= important;
+ }
+ if (!displayed)
+ refresh_prompt (ctx);
+}
+
+static void
+log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
+ unsigned flags, enum buffer_line_rendition r, const char *format, ...)
+{
+ va_list ap;
+ va_start (ap, format);
+
+ struct formatter f = formatter_make (ctx, s);
+ formatter_addv (&f, format, &ap);
+ log_formatter (ctx, buffer, flags, r, &f);
+
+ va_end (ap);
+}
+
+#define log_global(ctx, flags, r, ...) \
+ log_full ((ctx), NULL, (ctx)->global_buffer, (flags), (r), __VA_ARGS__)
+#define log_server(s, buffer, flags, r, ...) \
+ log_full ((s)->ctx, (s), (buffer), (flags), (r), __VA_ARGS__)
+
+#define log_global_status(ctx, ...) \
+ log_global ((ctx), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
+#define log_global_error(ctx, ...) \
+ log_global ((ctx), 0, BUFFER_LINE_ERROR, __VA_ARGS__)
+#define log_global_indent(ctx, ...) \
+ log_global ((ctx), 0, BUFFER_LINE_INDENT, __VA_ARGS__)
+
+#define log_server_status(s, buffer, ...) \
+ log_server ((s), (buffer), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
+#define log_server_error(s, buffer, ...) \
+ log_server ((s), (buffer), 0, BUFFER_LINE_ERROR, __VA_ARGS__)
+
+#define log_global_debug(ctx, ...) \
+ BLOCK_START \
+ if (g_debug_mode) \
+ log_global ((ctx), 0, 0, "(*) " __VA_ARGS__); \
+ BLOCK_END
+
+#define log_server_debug(s, ...) \
+ BLOCK_START \
+ if (g_debug_mode) \
+ log_server ((s), (s)->buffer, 0, 0, "(*) " __VA_ARGS__); \
+ BLOCK_END
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Lines that are used in more than one place
+
+#define log_nick_self(s, buffer, new_) \
+ log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
+ "You are now known as #n", (new_))
+#define log_nick(s, buffer, old, new_) \
+ log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
+ "#n is now known as #n", (old), (new_))
+
+#define log_chghost_self(s, buffer, new_) \
+ log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
+ "You are now #N", (new_))
+#define log_chghost(s, buffer, old, new_) \
+ log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
+ "#N is now #N", (old), (new_))
+
+#define log_outcoming_notice(s, buffer, who, text) \
+ log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text))
+#define log_outcoming_privmsg(s, buffer, prefixes, who, text) \
+ log_server ((s), (buffer), 0, 0, "<#s#n> #m", (prefixes), (who), (text))
+#define log_outcoming_action(s, buffer, who, text) \
+ log_server ((s), (buffer), 0, BUFFER_LINE_ACTION, "#n #m", (who), (text))
+
+#define log_outcoming_orphan_notice(s, target, text) \
+ log_server_status ((s), (s)->buffer, "Notice -> #n: #m", (target), (text))
+#define log_outcoming_orphan_privmsg(s, target, text) \
+ log_server ((s), (s)->buffer, 0, BUFFER_LINE_STATUS, \
+ "MSG(#n): #m", (target), (text))
+#define log_outcoming_orphan_action(s, target, text) \
+ log_server ((s), (s)->buffer, 0, BUFFER_LINE_ACTION, \
+ "MSG(#n): #m", (target), (text))
+
+#define log_ctcp_query(s, target, tag) \
+ log_server_status ((s), (s)->buffer, "CTCP query to #S: #S", target, tag)
+#define log_ctcp_reply(s, target, reply /* freed! */) \
+ log_server_status ((s), (s)->buffer, "CTCP reply to #S: #&S", target, reply)
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+make_log_filename (const char *filename, struct str *output)
+{
+ for (const char *p = filename; *p; p++)
+ // XXX: anything more to replace?
+ if (strchr ("/\\ ", *p))
+ str_append_c (output, '_');
+ else
+ str_append_c (output, tolower_ascii (*p));
+}
+
+static char *
+buffer_get_log_path (struct buffer *buffer)
+{
+ struct str path = str_make ();
+ get_xdg_home_dir (&path, "XDG_DATA_HOME", ".local/share");
+ str_append_printf (&path, "/%s/%s", PROGRAM_NAME, "logs");
+
+ (void) mkdir_with_parents (path.str, NULL);
+
+ str_append_c (&path, '/');
+ // FIXME: This mixes up character encodings.
+ make_log_filename (buffer->name, &path);
+ str_append (&path, ".log");
+ return str_steal (&path);
+}
+
+static void
+buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
+{
+ if (!ctx->logging || buffer->log_file)
+ return;
+
+ // TODO: should we try to reopen files wrt. case mapping?
+ // - Need to read the whole directory and look for matches:
+ // irc_server_strcmp(buffer->s, d_name, make_log_filename())
+ // remember to strip the ".log" suffix from d_name, case-sensitively.
+ // - The tolower_ascii() in make_log_filename() is a perfect overlap,
+ // it may stay as-is.
+ // - buffer_get_log_path() will need to return a FILE *,
+ // or an error that includes the below message.
+ char *path = buffer_get_log_path (buffer);
+ if (!(buffer->log_file = fopen (path, "ab")))
+ log_global_error (ctx, "Couldn't open log file `#l': #l",
+ path, strerror (errno));
+ else
+ set_cloexec (fileno (buffer->log_file));
+ free (path);
+}
+
+static void
+buffer_close_log_file (struct buffer *buffer)
+{
+ if (buffer->log_file)
+ (void) fclose (buffer->log_file);
+ buffer->log_file = NULL;
+}
+
+static void
+on_config_logging_change (struct config_item *item)
+{
+ struct app_context *ctx = item->user_data;
+ ctx->logging = item->value.boolean;
+
+ for (struct buffer *buffer = ctx->buffers; buffer; buffer = buffer->next)
+ if (ctx->logging)
+ buffer_open_log_file (ctx, buffer);
+ else
+ buffer_close_log_file (buffer);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct buffer *
+buffer_by_name (struct app_context *ctx, const char *name)
+{
+ return str_map_find (&ctx->buffers_by_name, name);
+}
+
+static void
+buffer_add (struct app_context *ctx, struct buffer *buffer)
+{
+ hard_assert (!buffer_by_name (ctx, buffer->name));
+
+ relay_prepare_buffer_update (ctx, buffer);
+ relay_broadcast (ctx);
+
+ str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
+ LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
+
+ buffer_open_log_file (ctx, buffer);
+
+ // Normally this doesn't cause changes in the prompt but a prompt hook
+ // could decide to show some information for all buffers nonetheless
+ refresh_prompt (ctx);
+}
+
+static void
+buffer_remove (struct app_context *ctx, struct buffer *buffer)
+{
+ hard_assert (buffer != ctx->current_buffer);
+ hard_assert (buffer != ctx->global_buffer);
+
+ relay_prepare_buffer_remove (ctx, buffer);
+ relay_broadcast (ctx);
+
+ CALL_ (ctx->input, buffer_destroy, buffer->input_data);
+ buffer->input_data = NULL;
+
+ // And make sure to unlink the buffer from "irc_buffer_map"
+ struct server *s = buffer->server;
+ if (buffer->channel)
+ str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL);
+ if (buffer->user)
+ str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);
+
+ if (buffer == ctx->last_buffer)
+ ctx->last_buffer = NULL;
+ if (buffer->type == BUFFER_SERVER)
+ buffer->server->buffer = NULL;
+
+ str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
+ LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
+ buffer_unref (buffer);
+
+ refresh_prompt (ctx);
+}
+
+static void
+buffer_print_read_marker (struct app_context *ctx, FILE *stream, int flush_opts)
+{
+ struct formatter f = formatter_make (ctx, NULL);
+ const int timestamp_width = 8; // hardcoded to %T right now, simple
+ const char *marker_char =
+ get_config_string (ctx->config.root, "general.read_marker_char");
+
+ // We could turn this off on FLUSH_OPT_NOWRAP, however our default pager
+ // wraps lines for us even if we don't do it ourselves, and thus there's
+ // no need to worry about inconsistency.
+ if (*marker_char)
+ {
+ struct str s = str_make ();
+ for (int i = 0; i < timestamp_width; i++)
+ str_append (&s, marker_char);
+ formatter_add (&f, "#a#s#r", ATTR_TIMESTAMP, s.str);
+ str_reset (&s);
+ for (int i = timestamp_width; i < g_terminal.columns; i++)
+ str_append (&s, marker_char);
+ formatter_add (&f, "#a#s#r\n", ATTR_READ_MARKER, s.str);
+ str_free (&s);
+ }
+ else
+ formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
+
+ formatter_flush (&f, stream, flush_opts);
+ // Flush the trailing formatting reset item
+ fflush (stream);
+ formatter_free (&f);
+}
+
+static void
+buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
+{
+ // Buffers can be activated, or their lines modified, as automatic actions.
+ if (ctx->terminal_suspended)
+ return;
+
+ // The prompt can take considerable time to redraw
+ CALL (ctx->input, hide);
+
+ // Simulate curses-like fullscreen buffers if the terminal allows it
+ if (g_terminal.initialized && clear_screen)
+ {
+ terminal_printer_fn printer = get_attribute_printer (stdout);
+ tputs (clear_screen, 1, printer);
+ if (cursor_to_ll)
+ tputs (cursor_to_ll, 1, printer);
+ else if (row_address)
+ tputs (tparm (row_address, g_terminal.lines - 1,
+ 0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
+ else if (cursor_address)
+ tputs (tparm (cursor_address, g_terminal.lines - 1,
+ 0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
+ fflush (stdout);
+
+ // We should update "last_displayed_msg_time" here just to be sure
+ // that the first date marker, if necessary, is shown, but in practice
+ // the value should always be from today when this function is called
+ }
+ else
+ {
+ char *buffer_name_localized =
+ iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL);
+ print_status ("%s", buffer_name_localized);
+ free (buffer_name_localized);
+ }
+
+ // That is, minus the readline prompt (taking at least one line)
+ int display_limit = MAX (10, g_terminal.lines - 1);
+ int to_display = 0;
+
+ struct buffer_line *line;
+ for (line = buffer->lines_tail; line; line = line->prev)
+ {
+ to_display++;
+ if (buffer_line_will_show_up (buffer, line))
+ display_limit--;
+ if (!line->prev || display_limit <= 0)
+ break;
+ }
+
+ // Once we've found where we want to start with the backlog, print it
+ int until_marker = to_display - (int) buffer->new_messages_count;
+ for (; line; line = line->next)
+ {
+ if (until_marker-- == 0
+ && buffer->new_messages_count != buffer->lines_count)
+ buffer_print_read_marker (ctx, stdout, 0);
+ buffer_line_display (ctx, buffer, line, 0);
+ }
+
+ // So that it is obvious if the last line in the buffer is not from today
+ buffer_update_time (ctx, time (NULL), stdout, 0);
+
+ refresh_prompt (ctx);
+ CALL (ctx->input, show);
+}
+
+static void
+buffer_activate (struct app_context *ctx, struct buffer *buffer)
+{
+ if (ctx->current_buffer == buffer)
+ return;
+
+ relay_prepare_buffer_activate (ctx, buffer);
+ relay_broadcast (ctx);
+
+ // This is the only place where the unread messages marker
+ // and highlight indicator are reset
+ if (ctx->current_buffer)
+ {
+ ctx->current_buffer->new_messages_count = 0;
+ ctx->current_buffer->new_unimportant_count = 0;
+ ctx->current_buffer->highlighted = false;
+ }
+
+ buffer_print_backlog (ctx, buffer);
+ CALL_ (ctx->input, buffer_switch, buffer->input_data);
+
+ // Now at last we can switch the pointers
+ ctx->last_buffer = ctx->current_buffer;
+ ctx->current_buffer = buffer;
+
+ refresh_prompt (ctx);
+}
+
+static void
+buffer_merge (struct app_context *ctx,
+ struct buffer *buffer, struct buffer *merged)
+{
+ // XXX: anything better to do? This situation is arguably rare and I'm
+ // not entirely sure what action to take.
+ log_full (ctx, NULL, buffer, 0, BUFFER_LINE_STATUS,
+ "Buffer #s was merged into this buffer", merged->name);
+
+ // Find all lines from "merged" newer than the newest line in "buffer"
+ struct buffer_line *start = merged->lines;
+ if (buffer->lines_tail)
+ while (start && start->when < buffer->lines_tail->when)
+ start = start->next;
+ if (!start)
+ return;
+
+ // Count how many of them we have
+ size_t n = 0;
+ for (struct buffer_line *iter = start; iter; iter = iter->next)
+ n++;
+ struct buffer_line *tail = merged->lines_tail;
+
+ // Cut them from the original buffer
+ if (start == merged->lines)
+ merged->lines = NULL;
+ else if (start->prev)
+ start->prev->next = NULL;
+ merged->lines_tail = start->prev;
+ merged->lines_count -= n;
+
+ // Append them to current lines in the buffer
+ buffer->lines_tail->next = start;
+ start->prev = buffer->lines_tail;
+ buffer->lines_tail = tail;
+ buffer->lines_count += n;
+
+ // And since there is no log_*() call, send them to relays manually
+ buffer->highlighted |= merged->highlighted;
+ LIST_FOR_EACH (struct buffer_line, line, start)
+ {
+ if (buffer->new_messages_count)
+ {
+ buffer->new_messages_count++;
+ if (line->flags & BUFFER_LINE_UNIMPORTANT)
+ buffer->new_unimportant_count++;
+ }
+
+ relay_prepare_buffer_line (ctx, buffer, line, false);
+ relay_broadcast (ctx);
+ }
+
+ log_full (ctx, NULL, buffer, BUFFER_LINE_SKIP_FILE, BUFFER_LINE_STATUS,
+ "End of merged content");
+}
+
+static void
+buffer_rename (struct app_context *ctx,
+ struct buffer *buffer, const char *new_name)
+{
+ struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name);
+ if (collision == buffer)
+ return;
+
+ hard_assert (!collision);
+
+ relay_prepare_buffer_rename (ctx, buffer, new_name);
+ relay_broadcast (ctx);
+
+ str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
+ str_map_set (&ctx->buffers_by_name, new_name, buffer);
+
+ cstr_set (&buffer->name, xstrdup (new_name));
+
+ buffer_close_log_file (buffer);
+ buffer_open_log_file (ctx, buffer);
+
+ // We might have renamed the current buffer
+ refresh_prompt (ctx);
+}
+
+static void
+buffer_clear (struct app_context *ctx, struct buffer *buffer)
+{
+ relay_prepare_buffer_clear (ctx, buffer);
+ relay_broadcast (ctx);
+
+ LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
+ buffer_line_destroy (iter);
+
+ buffer->lines = buffer->lines_tail = NULL;
+ buffer->lines_count = 0;
+}
+
+static void
+buffer_toggle_unimportant (struct app_context *ctx, struct buffer *buffer)
+{
+ buffer->hide_unimportant ^= true;
+
+ relay_prepare_buffer_update (ctx, buffer);
+ relay_broadcast (ctx);
+
+ if (buffer == ctx->current_buffer)
+ buffer_print_backlog (ctx, buffer);
+}
+
+static struct buffer *
+buffer_at_index (struct app_context *ctx, int n)
+{
+ int i = 0;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ if (++i == n)
+ return iter;
+ return NULL;
+}
+
+static struct buffer *
+buffer_next (struct app_context *ctx, int count)
+{
+ struct buffer *new_buffer = ctx->current_buffer;
+ while (count-- > 0)
+ if (!(new_buffer = new_buffer->next))
+ new_buffer = ctx->buffers;
+ return new_buffer;
+}
+
+static struct buffer *
+buffer_previous (struct app_context *ctx, int count)
+{
+ struct buffer *new_buffer = ctx->current_buffer;
+ while (count-- > 0)
+ if (!(new_buffer = new_buffer->prev))
+ new_buffer = ctx->buffers_tail;
+ return new_buffer;
+}
+
+static bool
+buffer_goto (struct app_context *ctx, int n)
+{
+ struct buffer *buffer = buffer_at_index (ctx, n);
+ if (!buffer)
+ return false;
+
+ buffer_activate (ctx, buffer);
+ return true;
+}
+
+static int
+buffer_count (struct app_context *ctx)
+{
+ int total = 0;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ total++;
+ return total;
+}
+
+static void
+buffer_move (struct app_context *ctx, struct buffer *buffer, int n)
+{
+ hard_assert (n >= 1 && n <= buffer_count (ctx));
+ LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
+
+ struct buffer *following = ctx->buffers;
+ while (--n && following)
+ following = following->next;
+
+ LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
+ refresh_prompt (ctx);
+}
+
+static int
+buffer_get_index (struct app_context *ctx, struct buffer *buffer)
+{
+ int index = 1;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ {
+ if (iter == buffer)
+ return index;
+ index++;
+ }
+ return -1;
+}
+
+static void
+buffer_remove_safe (struct app_context *ctx, struct buffer *buffer)
+{
+ if (buffer == ctx->current_buffer)
+ buffer_activate (ctx, ctx->last_buffer
+ ? ctx->last_buffer
+ : buffer_next (ctx, 1));
+ buffer_remove (ctx, buffer);
+}
+
+static void
+init_global_buffer (struct app_context *ctx)
+{
+ struct buffer *global = ctx->global_buffer =
+ buffer_new (ctx->input, BUFFER_GLOBAL, xstrdup (PROGRAM_NAME));
+
+ buffer_add (ctx, global);
+ buffer_activate (ctx, global);
+}
+
+// --- Users, channels ---------------------------------------------------------
+
+static char *
+irc_make_buffer_name (struct server *s, const char *target)
+{
+ if (!target)
+ return xstrdup (s->name);
+
+ char *target_utf8 = irc_to_utf8 (target);
+ char *name = xstrdup_printf ("%s.%s", s->name, target_utf8);
+ free (target_utf8);
+
+ struct buffer *conflict = buffer_by_name (s->ctx, name);
+ if (!conflict)
+ return name;
+
+ hard_assert (conflict->server == s);
+
+ // Fix up any conflicts. Note that while parentheses aren't allowed
+ // in IRC nicknames, they may occur in channel names.
+ int i = 0;
+ char *unique = xstrdup_printf ("%s(%d)", name, ++i);
+ while (buffer_by_name (s->ctx, unique))
+ cstr_set (&unique, xstrdup_printf ("%s(%d)", name, ++i));
+ free (name);
+ return unique;
+}
+
+static void
+irc_user_on_destroy (void *object, void *user_data)
+{
+ struct user *user = object;
+ struct server *s = user_data;
+ if (!s->rehashing)
+ str_map_set (&s->irc_users, user->nickname, NULL);
+}
+
+static struct user *
+irc_make_user (struct server *s, char *nickname)
+{
+ hard_assert (!str_map_find (&s->irc_users, nickname));
+
+ struct user *user = user_new (nickname);
+ (void) user_weak_ref (user, irc_user_on_destroy, s);
+ str_map_set (&s->irc_users, user->nickname, user);
+ return user;
+}
+
+struct user *
+irc_get_or_make_user (struct server *s, const char *nickname)
+{
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ if (user)
+ return user_ref (user);
+ return irc_make_user (s, xstrdup (nickname));
+}
+
+static struct buffer *
+irc_get_or_make_user_buffer (struct server *s, const char *nickname)
+{
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, nickname);
+ if (buffer)
+ return buffer;
+
+ struct user *user = irc_get_or_make_user (s, nickname);
+
+ // Open a new buffer for the user
+ buffer = buffer_new (s->ctx->input,
+ BUFFER_PM, irc_make_buffer_name (s, nickname));
+ buffer->server = s;
+ buffer->user = user;
+ str_map_set (&s->irc_buffer_map, user->nickname, buffer);
+
+ buffer_add (s->ctx, buffer);
+ return buffer;
+}
+
+static void
+irc_get_channel_user_prefix (struct server *s,
+ struct channel_user *channel_user, struct str *output)
+{
+ if (s->ctx->show_all_prefixes)
+ str_append (output, channel_user->prefixes);
+ else if (channel_user->prefixes[0])
+ str_append_c (output, channel_user->prefixes[0]);
+}
+
+static bool
+irc_channel_is_joined (struct channel *channel)
+{
+ // TODO: find a better way of checking if we're on a channel
+ return !!channel->users_len;
+}
+
+// Note that this eats the user reference
+static void
+irc_channel_link_user (struct channel *channel, struct user *user,
+ const char *prefixes)
+{
+ struct user_channel *user_channel = user_channel_new (channel);
+ LIST_PREPEND (user->channels, user_channel);
+
+ struct channel_user *channel_user = channel_user_new (user, prefixes);
+ LIST_PREPEND (channel->users, channel_user);
+ channel->users_len++;
+}
+
+static void
+irc_channel_unlink_user
+ (struct channel *channel, struct channel_user *channel_user)
+{
+ // First destroy the user's weak references to the channel
+ struct user *user = channel_user->user;
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ if (iter->channel == channel)
+ {
+ LIST_UNLINK (user->channels, iter);
+ user_channel_destroy (iter);
+ }
+
+ // TODO: poll the away status for users we don't share a channel with.
+ // It might or might not be worth to auto-set this on with RPL_AWAY.
+ if (!user->channels && user != channel->s->irc_user)
+ user->away = false;
+
+ // Then just unlink the user from the channel
+ LIST_UNLINK (channel->users, channel_user);
+ channel_user_destroy (channel_user);
+ channel->users_len--;
+}
+
+static void
+irc_channel_on_destroy (void *object, void *user_data)
+{
+ struct channel *channel = object;
+ struct server *s = user_data;
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ irc_channel_unlink_user (channel, iter);
+ if (!s->rehashing)
+ str_map_set (&s->irc_channels, channel->name, NULL);
+}
+
+static struct channel *
+irc_make_channel (struct server *s, char *name)
+{
+ hard_assert (!str_map_find (&s->irc_channels, name));
+
+ struct channel *channel = channel_new (s, name);
+ (void) channel_weak_ref (channel, irc_channel_on_destroy, s);
+ str_map_set (&s->irc_channels, channel->name, channel);
+ return channel;
+}
+
+static void
+irc_channel_broadcast_buffer_update (const struct channel *channel)
+{
+ struct server *s = channel->s;
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
+ if (buffer)
+ {
+ relay_prepare_buffer_update (s->ctx, buffer);
+ relay_broadcast (s->ctx);
+ }
+}
+
+static void
+irc_channel_set_topic (struct channel *channel, const char *topic)
+{
+ cstr_set (&channel->topic, xstrdup (topic));
+ irc_channel_broadcast_buffer_update (channel);
+}
+
+static struct channel_user *
+irc_channel_get_user (struct channel *channel, struct user *user)
+{
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ if (iter->user == user)
+ return iter;
+ return NULL;
+}
+
+static void
+irc_remove_user_from_channel (struct user *user, struct channel *channel)
+{
+ struct channel_user *channel_user = irc_channel_get_user (channel, user);
+ if (channel_user)
+ irc_channel_unlink_user (channel, channel_user);
+}
+
+static void
+irc_left_channel (struct channel *channel)
+{
+ strv_reset (&channel->names_buf);
+ channel->show_names_after_who = false;
+
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ irc_channel_unlink_user (channel, iter);
+
+ // Send empty channel modes.
+ irc_channel_broadcast_buffer_update (channel);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+remove_conflicting_buffer (struct server *s, struct buffer *buffer)
+{
+ log_server_status (s, s->buffer,
+ "Removed buffer #s because of casemapping conflict", buffer->name);
+ if (s->ctx->current_buffer == buffer)
+ buffer_activate (s->ctx, s->buffer);
+ buffer_remove (s->ctx, buffer);
+}
+
+static void
+irc_try_readd_user (struct server *s,
+ struct user *user, struct buffer *buffer)
+{
+ if (str_map_find (&s->irc_users, user->nickname))
+ {
+ // Remove user from all channels and destroy any PM buffer
+ user_ref (user);
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ irc_remove_user_from_channel (user, iter->channel);
+ if (buffer)
+ remove_conflicting_buffer (s, buffer);
+ user_unref (user);
+ }
+ else
+ {
+ str_map_set (&s->irc_users, user->nickname, user);
+ str_map_set (&s->irc_buffer_map, user->nickname, buffer);
+ }
+}
+
+static void
+irc_try_readd_channel (struct channel *channel, struct buffer *buffer)
+{
+ struct server *s = channel->s;
+ if (str_map_find (&s->irc_channels, channel->name))
+ {
+ // Remove all users from channel and destroy any channel buffer
+ channel_ref (channel);
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ irc_channel_unlink_user (channel, iter);
+ if (buffer)
+ remove_conflicting_buffer (s, buffer);
+ channel_unref (channel);
+ }
+ else
+ {
+ str_map_set (&s->irc_channels, channel->name, channel);
+ str_map_set (&s->irc_buffer_map, channel->name, buffer);
+ }
+}
+
+static void
+irc_rehash_and_fix_conflicts (struct server *s)
+{
+ // Save the old maps and initialize new ones
+ struct str_map old_users = s->irc_users;
+ struct str_map old_channels = s->irc_channels;
+ struct str_map old_buffer_map = s->irc_buffer_map;
+
+ s->irc_users = str_map_make (NULL);
+ s->irc_channels = str_map_make (NULL);
+ s->irc_buffer_map = str_map_make (NULL);
+
+ s->irc_users .key_xfrm = s->irc_strxfrm;
+ s->irc_channels .key_xfrm = s->irc_strxfrm;
+ s->irc_buffer_map.key_xfrm = s->irc_strxfrm;
+
+ // Prevent channels and users from unsetting themselves
+ // from server maps upon removing the last reference to them
+ s->rehashing = true;
+
+ // XXX: to be perfectly sure, we should also check
+ // whether any users collide with channels and vice versa
+
+ // Our own user always takes priority, add him first
+ if (s->irc_user)
+ irc_try_readd_user (s, s->irc_user,
+ str_map_find (&old_buffer_map, s->irc_user->nickname));
+
+ struct str_map_iter iter;
+ struct user *user;
+ struct channel *channel;
+
+ iter = str_map_iter_make (&old_users);
+ while ((user = str_map_iter_next (&iter)))
+ irc_try_readd_user (s, user,
+ str_map_find (&old_buffer_map, user->nickname));
+
+ iter = str_map_iter_make (&old_channels);
+ while ((channel = str_map_iter_next (&iter)))
+ irc_try_readd_channel (channel,
+ str_map_find (&old_buffer_map, channel->name));
+
+ // Hopefully we've either moved or destroyed all the old content
+ s->rehashing = false;
+
+ str_map_free (&old_users);
+ str_map_free (&old_channels);
+ str_map_free (&old_buffer_map);
+}
+
+static void
+irc_set_casemapping (struct server *s,
+ irc_tolower_fn tolower, irc_strxfrm_fn strxfrm)
+{
+ if (tolower == s->irc_tolower
+ && strxfrm == s->irc_strxfrm)
+ return;
+
+ s->irc_tolower = tolower;
+ s->irc_strxfrm = strxfrm;
+
+ // Ideally we would never have to do this but I can't think of a workaround
+ irc_rehash_and_fix_conflicts (s);
+}
+
+// --- Core functionality ------------------------------------------------------
+
+static bool
+irc_is_connected (struct server *s)
+{
+ return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
+}
+
+static void
+irc_update_poller (struct server *s, const struct pollfd *pfd)
+{
+ int new_events = s->transport->get_poll_events (s);
+ hard_assert (new_events != 0);
+
+ if (!pfd || pfd->events != new_events)
+ poller_fd_set (&s->socket_event, new_events);
+}
+
+static void
+irc_cancel_timers (struct server *s)
+{
+ poller_timer_reset (&s->timeout_tmr);
+ poller_timer_reset (&s->ping_tmr);
+ poller_timer_reset (&s->reconnect_tmr);
+ poller_timer_reset (&s->autojoin_tmr);
+}
+
+static void
+irc_reset_connection_timeouts (struct server *s)
+{
+ poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
+ poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
+ poller_timer_reset (&s->reconnect_tmr);
+}
+
+static int64_t
+irc_get_reconnect_delay (struct server *s)
+{
+ int64_t delay = get_config_integer (s->config, "reconnect_delay");
+ int64_t delay_factor = get_config_integer
+ (s->ctx->config.root, "general.reconnect_delay_growing");
+ for (unsigned i = 0; i < s->reconnect_attempt; i++)
+ {
+ if (delay_factor && delay > INT64_MAX / delay_factor)
+ break;
+ delay *= delay_factor;
+ }
+
+ int64_t delay_max =
+ get_config_integer (s->ctx->config.root, "general.reconnect_delay_max");
+ return MIN (delay, delay_max);
+}
+
+static void
+irc_queue_reconnect (struct server *s)
+{
+ // As long as the user wants us to, that is
+ if (!get_config_boolean (s->config, "reconnect"))
+ return;
+
+ // XXX: maybe add a state for when a connect is queued?
+ hard_assert (s->state == IRC_DISCONNECTED);
+
+ int64_t delay = irc_get_reconnect_delay (s);
+ s->reconnect_attempt++;
+
+ log_server_status (s, s->buffer,
+ "Trying to reconnect in #&s seconds...",
+ xstrdup_printf ("%" PRId64, delay));
+ poller_timer_set (&s->reconnect_tmr, delay * 1000);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void irc_process_sent_message
+ (const struct irc_message *msg, struct server *s);
+static void irc_send (struct server *s,
+ const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
+
+static void
+irc_send (struct server *s, const char *format, ...)
+{
+ if (!soft_assert (irc_is_connected (s)))
+ {
+ log_server_debug (s, "sending a message to a dead server connection");
+ return;
+ }
+
+ if (s->state == IRC_CLOSING
+ || s->state == IRC_HALF_CLOSED)
+ return;
+
+ va_list ap;
+ va_start (ap, format);
+ struct str str = str_make ();
+ str_append_vprintf (&str, format, ap);
+ va_end (ap);
+
+ log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);
+
+ struct irc_message msg;
+ irc_parse_message (&msg, str.str);
+ irc_process_sent_message (&msg, s);
+ irc_free_message (&msg);
+
+ str_append_str (&s->write_buffer, &str);
+ str_free (&str);
+ str_append (&s->write_buffer, "\r\n");
+ irc_update_poller (s, NULL);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_set_state (struct server *s, enum server_state state)
+{
+ s->state = state;
+
+ relay_prepare_server_update (s->ctx, s);
+ relay_broadcast (s->ctx);
+
+ refresh_prompt (s->ctx);
+}
+
+static void
+irc_real_shutdown (struct server *s)
+{
+ hard_assert (irc_is_connected (s) && s->state != IRC_HALF_CLOSED);
+
+ if (s->transport
+ && s->transport->in_before_shutdown)
+ s->transport->in_before_shutdown (s);
+
+ while (shutdown (s->socket, SHUT_WR) == -1)
+ // XXX: we get ENOTCONN with OpenSSL (not plain) when a localhost
+ // server is aborted, why? strace says read 0, write 31, shutdown -1.
+ if (!soft_assert (errno == EINTR))
+ break;
+
+ irc_set_state (s, IRC_HALF_CLOSED);
+}
+
+static void
+irc_shutdown (struct server *s)
+{
+ if (s->state == IRC_CLOSING
+ || s->state == IRC_HALF_CLOSED)
+ return;
+
+ // TODO: set a timer to cut the connection if we don't receive an EOF
+ irc_set_state (s, IRC_CLOSING);
+
+ // Either there's still some data in the write buffer and we wait
+ // until they're sent, or we send an EOF to the server right away
+ if (!s->write_buffer.len)
+ irc_real_shutdown (s);
+}
+
+static void
+irc_destroy_connector (struct server *s)
+{
+ if (s->connector)
+ connector_free (s->connector);
+ free (s->connector);
+ s->connector = NULL;
+
+ if (s->socks_conn)
+ socks_connector_free (s->socks_conn);
+ free (s->socks_conn);
+ s->socks_conn = NULL;
+
+ // Not connecting anymore
+ irc_set_state (s, IRC_DISCONNECTED);
+}
+
+static void
+try_finish_quit (struct app_context *ctx)
+{
+ if (!ctx->quitting)
+ return;
+
+ bool disconnected_all = true;
+ struct str_map_iter iter = str_map_iter_make (&ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ if (irc_is_connected (s))
+ disconnected_all = false;
+
+ if (disconnected_all)
+ ctx->polling = false;
+}
+
+static void
+irc_destroy_transport (struct server *s)
+{
+ if (s->transport
+ && s->transport->cleanup)
+ s->transport->cleanup (s);
+ s->transport = NULL;
+
+ poller_fd_reset (&s->socket_event);
+ xclose (s->socket);
+ s->socket = -1;
+ irc_set_state (s, IRC_DISCONNECTED);
+
+ str_reset (&s->read_buffer);
+ str_reset (&s->write_buffer);
+}
+
+static void
+irc_destroy_state (struct server *s)
+{
+ struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
+ struct channel *channel;
+ while ((channel = str_map_iter_next (&iter)))
+ irc_left_channel (channel);
+
+ if (s->irc_user)
+ {
+ user_unref (s->irc_user);
+ s->irc_user = NULL;
+ }
+
+ str_reset (&s->irc_user_modes);
+ cstr_set (&s->irc_user_host, NULL);
+
+ strv_reset (&s->outstanding_joins);
+ strv_reset (&s->cap_ls_buf);
+ s->cap_away_notify = false;
+ s->cap_echo_message = false;
+ s->cap_sasl = false;
+
+ // Need to call this before server_init_specifics()
+ irc_set_casemapping (s, irc_tolower, irc_strxfrm);
+
+ server_free_specifics (s);
+ server_init_specifics (s);
+}
+
+static void
+irc_disconnect (struct server *s)
+{
+ hard_assert (irc_is_connected (s));
+
+ struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
+ struct buffer *buffer;
+ while ((buffer = str_map_iter_next (&iter)))
+ log_server (s, buffer, BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,
+ "Disconnected from server");
+
+ irc_cancel_timers (s);
+ irc_destroy_transport (s);
+ irc_destroy_state (s);
+
+ // Take any relevant actions
+ if (s->ctx->quitting)
+ try_finish_quit (s->ctx);
+ else if (s->manual_disconnect)
+ s->manual_disconnect = false;
+ else
+ {
+ s->reconnect_attempt = 0;
+ irc_queue_reconnect (s);
+ }
+}
+
+static void
+irc_initiate_disconnect (struct server *s, const char *reason)
+{
+ hard_assert (irc_is_connected (s));
+
+ // It can take a very long time for sending QUIT to take effect
+ if (s->manual_disconnect)
+ {
+ log_server_error (s, s->buffer, "#s: #s", "Disconnected from server",
+ "connection torn down early per user request");
+ irc_disconnect (s);
+ return;
+ }
+
+ if (reason)
+ irc_send (s, "QUIT :%s", reason);
+ else
+ // TODO: make the default QUIT message customizable
+ // -> global/per server/both?
+ // -> implement it with an output hook in a plugin?
+ irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
+
+ s->manual_disconnect = true;
+ irc_shutdown (s);
+}
+
+static void
+request_quit (struct app_context *ctx, const char *message)
+{
+ if (!ctx->quitting)
+ {
+ log_global_status (ctx, "Shutting down");
+ ctx->quitting = true;
+
+ // Disable the user interface
+ CALL (ctx->input, hide);
+ }
+
+ struct str_map_iter iter = str_map_iter_make (&ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ {
+ // There may be a timer set to reconnect to the server
+ poller_timer_reset (&s->reconnect_tmr);
+
+ if (irc_is_connected (s))
+ irc_initiate_disconnect (s, message);
+ else if (s->state == IRC_CONNECTING)
+ irc_destroy_connector (s);
+ }
+
+ try_finish_quit (ctx);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+on_irc_ping_timeout (void *user_data)
+{
+ struct server *s = user_data;
+ log_server_error (s, s->buffer,
+ "#s: #s", "Disconnected from server", "timeout");
+ irc_disconnect (s);
+}
+
+static void
+on_irc_timeout (void *user_data)
+{
+ // Provoke a response from the server
+ struct server *s = user_data;
+ irc_send (s, "PING :%" PRIi64, (int64_t) time (NULL));
+}
+
+static void
+on_irc_autojoin_timeout (void *user_data)
+{
+ struct server *s = user_data;
+
+ // Since we may not have information from RPL_ISUPPORT yet,
+ // it's our safest bet to send the channels one at a time
+
+ struct str_map joins_sent = str_map_make (NULL);
+
+ // We don't know the casemapping yet either, however ASCII should do
+ joins_sent.key_xfrm = tolower_ascii_strxfrm;
+
+ // First join autojoin channels in their given order
+ const char *autojoin = get_config_string (s->config, "autojoin");
+ if (autojoin)
+ {
+ struct strv v = strv_make ();
+ cstr_split (autojoin, ",", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ {
+ irc_send (s, "JOIN %s", v.vector[i]);
+ str_map_set (&joins_sent, v.vector[i], (void *) 1);
+ }
+ strv_free (&v);
+ }
+
+ // Then also rejoin any channels from the last disconnect
+ struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
+ struct channel *channel;
+ while ((channel = str_map_iter_next (&iter)))
+ {
+ struct str target = str_make ();
+ str_append (&target, channel->name);
+
+ const char *key;
+ if ((key = str_map_find (&channel->param_modes, "k")))
+ str_append_printf (&target, " %s", key);
+
+ // When a channel is both autojoined and rejoined, both keys are tried
+ if (!channel->left_manually
+ && !str_map_find (&joins_sent, target.str))
+ irc_send (s, "JOIN %s", target.str);
+ str_free (&target);
+ }
+
+ str_map_free (&joins_sent);
+}
+
+// --- Server I/O --------------------------------------------------------------
+
+static char *
+irc_process_hooks (struct server *s, char *input)
+{
+ log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, input);
+ uint64_t hash = siphash_wrapper (input, strlen (input));
+ LIST_FOR_EACH (struct hook, iter, s->ctx->irc_hooks)
+ {
+ struct irc_hook *hook = (struct irc_hook *) iter;
+ if (!(input = hook->filter (hook, s, input)))
+ {
+ log_server_debug (s, "#a>= #s#r", ATTR_JOIN, "thrown away by hook");
+ return NULL;
+ }
+
+ // The old input may get freed, so we compare against a hash of it
+ uint64_t new_hash = siphash_wrapper (input, strlen (input));
+ if (new_hash != hash)
+ log_server_debug (s, "#a>= \"#S\"#r", ATTR_JOIN, input);
+ hash = new_hash;
+ }
+ return input;
+}
+
+static void irc_process_message
+ (const struct irc_message *msg, struct server *s);
+
+static void
+irc_process_buffer_custom (struct server *s, struct str *buf)
+{
+ const char *start = buf->str, *end = start + buf->len;
+ for (const char *p = start; p + 1 < end; p++)
+ {
+ // Split the input on newlines
+ if (p[0] != '\r' || p[1] != '\n')
+ continue;
+
+ char *processed = irc_process_hooks (s, xstrndup (start, p - start));
+ start = p + 2;
+ if (!processed)
+ continue;
+
+ struct irc_message msg;
+ irc_parse_message (&msg, processed);
+ irc_process_message (&msg, s);
+ irc_free_message (&msg);
+
+ free (processed);
+ }
+ str_remove_slice (buf, 0, start - buf->str);
+}
+
+static enum socket_io_result
+irc_try_read (struct server *s)
+{
+ enum socket_io_result result = s->transport->try_read (s);
+ if (s->read_buffer.len >= (1 << 20))
+ {
+ // XXX: this is stupid; if anything, count it in dependence of time;
+ // we could make transport_tls_try_read() limit the immediate amount
+ // of data read like socket_io_try_read() does and remove this check
+ log_server_error (s, s->buffer,
+ "The IRC server seems to spew out data frantically");
+ return SOCKET_IO_ERROR;
+ }
+ if (s->read_buffer.len)
+ irc_process_buffer_custom (s, &s->read_buffer);
+ return result;
+}
+
+static enum socket_io_result
+irc_try_write (struct server *s)
+{
+ enum socket_io_result result = s->transport->try_write (s);
+ if (result == SOCKET_IO_OK)
+ {
+ // If we're flushing the write buffer and our job is complete, we send
+ // an EOF to the server, changing the state to IRC_HALF_CLOSED
+ if (s->state == IRC_CLOSING && !s->write_buffer.len)
+ irc_real_shutdown (s);
+ }
+ return result;
+}
+
+static bool
+irc_try_read_write (struct server *s)
+{
+ enum socket_io_result read_result;
+ enum socket_io_result write_result;
+ if ((read_result = irc_try_read (s)) == SOCKET_IO_ERROR
+ || (write_result = irc_try_write (s)) == SOCKET_IO_ERROR)
+ {
+ log_server_error (s, s->buffer, "Server connection failed");
+ return false;
+ }
+
+ // FIXME: this may probably fire multiple times when we're flushing,
+ // we should probably store a flag next to the state
+ if (read_result == SOCKET_IO_EOF
+ || write_result == SOCKET_IO_EOF)
+ log_server_error (s, s->buffer, "Server closed the connection");
+
+ // If the write needs to read and we receive an EOF, we can't flush
+ if (write_result == SOCKET_IO_EOF)
+ return false;
+
+ if (read_result == SOCKET_IO_EOF)
+ {
+ // Eventually initiate shutdown to flush the write buffer
+ irc_shutdown (s);
+
+ // If there's nothing to write, we can disconnect now
+ if (s->state == IRC_HALF_CLOSED)
+ return false;
+ }
+ return true;
+}
+
+static void
+on_irc_ready (const struct pollfd *pfd, struct server *s)
+{
+ if (irc_try_read_write (s))
+ {
+ // XXX: shouldn't we rather wait for PONG messages?
+ irc_reset_connection_timeouts (s);
+ irc_update_poller (s, pfd);
+ }
+ else
+ // We don't want to keep the socket anymore
+ irc_disconnect (s);
+}
+
+// --- Plain transport ---------------------------------------------------------
+
+static enum socket_io_result
+transport_plain_try_read (struct server *s)
+{
+ enum socket_io_result result =
+ socket_io_try_read (s->socket, &s->read_buffer);
+ if (result == SOCKET_IO_ERROR)
+ print_debug ("%s: %s", __func__, strerror (errno));
+ return result;
+}
+
+static enum socket_io_result
+transport_plain_try_write (struct server *s)
+{
+ enum socket_io_result result =
+ socket_io_try_write (s->socket, &s->write_buffer);
+ if (result == SOCKET_IO_ERROR)
+ print_debug ("%s: %s", __func__, strerror (errno));
+ return result;
+}
+
+static int
+transport_plain_get_poll_events (struct server *s)
+{
+ int events = POLLIN;
+ if (s->write_buffer.len)
+ events |= POLLOUT;
+ return events;
+}
+
+static struct transport g_transport_plain =
+{
+ .try_read = transport_plain_try_read,
+ .try_write = transport_plain_try_write,
+ .get_poll_events = transport_plain_get_poll_events,
+};
+
+// --- TLS transport -----------------------------------------------------------
+
+struct transport_tls_data
+{
+ SSL_CTX *ssl_ctx; ///< SSL context
+ SSL *ssl; ///< SSL connection
+ bool ssl_rx_want_tx; ///< SSL_read() wants to write
+ bool ssl_tx_want_rx; ///< SSL_write() wants to read
+};
+
+/// The index in SSL_CTX user data for a reference to the server
+static int g_transport_tls_data_index = -1;
+
+static int
+transport_tls_verify_callback (int preverify_ok, X509_STORE_CTX *ctx)
+{
+ SSL *ssl = X509_STORE_CTX_get_ex_data
+ (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ());
+ struct server *s = SSL_CTX_get_ex_data
+ (SSL_get_SSL_CTX (ssl), g_transport_tls_data_index);
+
+ X509 *cert = X509_STORE_CTX_get_current_cert (ctx);
+ char *subject = X509_NAME_oneline (X509_get_subject_name (cert), NULL, 0);
+ char *issuer = X509_NAME_oneline (X509_get_issuer_name (cert), NULL, 0);
+
+ log_server_status (s, s->buffer, "Certificate subject: #s", subject);
+ log_server_status (s, s->buffer, "Certificate issuer: #s", issuer);
+
+ if (!preverify_ok)
+ {
+ log_server_error (s, s->buffer,
+ "Certificate verification failed: #s",
+ X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx)));
+ }
+
+ free (subject);
+ free (issuer);
+ return preverify_ok;
+}
+
+static bool
+transport_tls_init_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
+ struct error **e)
+{
+ ERR_clear_error ();
+
+ if (file || path)
+ {
+ if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
+ return true;
+
+ return error_set (e, "%s: %s",
+ "Failed to set locations for the CA certificate bundle",
+ xerr_describe_error ());
+ }
+
+ if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
+ return error_set (e, "%s: %s",
+ "Couldn't load the default CA certificate bundle",
+ xerr_describe_error ());
+ return true;
+}
+
+static bool
+transport_tls_init_ca (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
+{
+ const char *ca_file = get_config_string (s->config, "tls_ca_file");
+ const char *ca_path = get_config_string (s->config, "tls_ca_path");
+
+ char *full_ca_file = ca_file
+ ? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
+ char *full_ca_path = ca_path
+ ? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
+
+ bool ok = false;
+ if (ca_file && !full_ca_file)
+ error_set (e, "Couldn't find the CA bundle file");
+ else if (ca_path && !full_ca_path)
+ error_set (e, "Couldn't find the CA bundle path");
+ else
+ ok = transport_tls_init_ca_set (ssl_ctx, full_ca_file, full_ca_path, e);
+
+ free (full_ca_file);
+ free (full_ca_path);
+ return ok;
+}
+
+static bool
+transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
+{
+ bool verify = get_config_boolean (s->config, "tls_verify");
+ SSL_CTX_set_verify (ssl_ctx, verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE,
+ transport_tls_verify_callback);
+
+ if (g_transport_tls_data_index == -1)
+ g_transport_tls_data_index =
+ SSL_CTX_get_ex_new_index (0, "server", NULL, NULL, NULL);
+ SSL_CTX_set_ex_data (ssl_ctx, g_transport_tls_data_index, s);
+
+ const char *ciphers = get_config_string (s->config, "tls_ciphers");
+ if (ciphers && !SSL_CTX_set_cipher_list (ssl_ctx, ciphers))
+ log_server_error (s, s->buffer,
+ "Failed to select any cipher from the cipher list");
+ SSL_CTX_set_mode (ssl_ctx,
+ SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+
+ // Disable deprecated protocols (see RFC 7568)
+ SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ // This seems to consume considerable amounts of memory while not giving
+ // that much in return; in addition to that, I'm not sure about security
+ // (see RFC 7525, section 3.3)
+#ifdef SSL_OP_NO_COMPRESSION
+ SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_COMPRESSION);
+#endif // SSL_OP_NO_COMPRESSION
+#ifdef LOMEM
+ SSL_CTX_set_mode (ssl_ctx, SSL_MODE_RELEASE_BUFFERS);
+#endif // LOMEM
+
+ struct error *error = NULL;
+ if (!transport_tls_init_ca (s, ssl_ctx, &error))
+ {
+ if (verify)
+ {
+ error_propagate (e, error);
+ return false;
+ }
+
+ // Just inform the user if we're not actually verifying
+ log_server_error (s, s->buffer, "#s", error->message);
+ error_free (error);
+ }
+ return true;
+}
+
+static bool
+transport_tls_init_cert (struct server *s, SSL *ssl, struct error **e)
+{
+ const char *tls_cert = get_config_string (s->config, "tls_cert");
+ if (!tls_cert)
+ return true;
+
+ ERR_clear_error ();
+
+ bool result = false;
+ char *path = resolve_filename (tls_cert, resolve_relative_config_filename);
+ if (!path)
+ error_set (e, "%s: %s", "Cannot open file", tls_cert);
+ // XXX: perhaps we should read the file ourselves for better messages
+ else if (!SSL_use_certificate_file (ssl, path, SSL_FILETYPE_PEM)
+ || !SSL_use_PrivateKey_file (ssl, path, SSL_FILETYPE_PEM))
+ error_set (e, "%s: %s", "Setting the TLS client certificate failed",
+ xerr_describe_error ());
+ else
+ result = true;
+ free (path);
+ return result;
+}
+
+static bool
+transport_tls_init (struct server *s, const char *hostname, struct error **e)
+{
+ ERR_clear_error ();
+
+ struct error *error = NULL;
+ SSL_CTX *ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
+ if (!ssl_ctx)
+ goto error_ssl_1;
+ if (!transport_tls_init_ctx (s, ssl_ctx, &error))
+ goto error_ssl_2;
+
+ SSL *ssl = SSL_new (ssl_ctx);
+ if (!ssl)
+ goto error_ssl_2;
+
+ if (!transport_tls_init_cert (s, ssl, &error))
+ {
+ // XXX: is this a reason to abort the connection?
+ log_server_error (s, s->buffer, "#s", error->message);
+ error_free (error);
+ error = NULL;
+ }
+
+ SSL_set_connect_state (ssl);
+ if (!SSL_set_fd (ssl, s->socket))
+ goto error_ssl_3;
+
+ // Enable SNI, FWIW; literal IP addresses aren't allowed
+ struct in6_addr dummy;
+ if (!inet_pton (AF_INET, hostname, &dummy)
+ && !inet_pton (AF_INET6, hostname, &dummy))
+ SSL_set_tlsext_host_name (ssl, hostname);
+
+ struct transport_tls_data *data = xcalloc (1, sizeof *data);
+ data->ssl_ctx = ssl_ctx;
+ data->ssl = ssl;
+
+ // Forces a handshake even if neither side wants to transmit data
+ data->ssl_rx_want_tx = true;
+
+ s->transport_data = data;
+ return true;
+
+error_ssl_3:
+ SSL_free (ssl);
+error_ssl_2:
+ SSL_CTX_free (ssl_ctx);
+error_ssl_1:
+ if (!error)
+ error_set (&error, "%s: %s", "Could not initialize TLS",
+ xerr_describe_error ());
+
+ error_propagate (e, error);
+ return false;
+}
+
+static void
+transport_tls_cleanup (struct server *s)
+{
+ struct transport_tls_data *data = s->transport_data;
+ if (data->ssl)
+ SSL_free (data->ssl);
+ if (data->ssl_ctx)
+ SSL_CTX_free (data->ssl_ctx);
+ free (data);
+}
+
+static enum socket_io_result
+transport_tls_try_read (struct server *s)
+{
+ struct transport_tls_data *data = s->transport_data;
+ if (data->ssl_tx_want_rx)
+ return SOCKET_IO_OK;
+
+ struct str *buf = &s->read_buffer;
+ data->ssl_rx_want_tx = false;
+ while (true)
+ {
+ ERR_clear_error ();
+ str_reserve (buf, 512);
+ int n_read = SSL_read (data->ssl, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (data->ssl, n_read, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ buf->str[buf->len += n_read] = '\0';
+ continue;
+ case SSL_ERROR_ZERO_RETURN:
+ return SOCKET_IO_EOF;
+ case SSL_ERROR_WANT_READ:
+ return SOCKET_IO_OK;
+ case SSL_ERROR_WANT_WRITE:
+ data->ssl_rx_want_tx = true;
+ return SOCKET_IO_OK;
+ case XSSL_ERROR_TRY_AGAIN:
+ continue;
+ default:
+ LOG_FUNC_FAILURE ("SSL_read", error_info);
+ return SOCKET_IO_ERROR;
+ }
+ }
+}
+
+static enum socket_io_result
+transport_tls_try_write (struct server *s)
+{
+ struct transport_tls_data *data = s->transport_data;
+ if (data->ssl_rx_want_tx)
+ return SOCKET_IO_OK;
+
+ struct str *buf = &s->write_buffer;
+ data->ssl_tx_want_rx = false;
+ while (buf->len)
+ {
+ ERR_clear_error ();
+ int n_written = SSL_write (data->ssl, buf->str, buf->len);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (data->ssl, n_written, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ str_remove_slice (buf, 0, n_written);
+ continue;
+ case SSL_ERROR_ZERO_RETURN:
+ return SOCKET_IO_EOF;
+ case SSL_ERROR_WANT_WRITE:
+ return SOCKET_IO_OK;
+ case SSL_ERROR_WANT_READ:
+ data->ssl_tx_want_rx = true;
+ return SOCKET_IO_OK;
+ case XSSL_ERROR_TRY_AGAIN:
+ continue;
+ default:
+ LOG_FUNC_FAILURE ("SSL_write", error_info);
+ return SOCKET_IO_ERROR;
+ }
+ }
+ return SOCKET_IO_OK;
+}
+
+static int
+transport_tls_get_poll_events (struct server *s)
+{
+ struct transport_tls_data *data = s->transport_data;
+
+ int events = POLLIN;
+ if (s->write_buffer.len || data->ssl_rx_want_tx)
+ events |= POLLOUT;
+
+ // While we're waiting for an opposite event, we ignore the original
+ if (data->ssl_rx_want_tx) events &= ~POLLIN;
+ if (data->ssl_tx_want_rx) events &= ~POLLOUT;
+ return events;
+}
+
+static void
+transport_tls_in_before_shutdown (struct server *s)
+{
+ struct transport_tls_data *data = s->transport_data;
+ (void) SSL_shutdown (data->ssl);
+}
+
+static struct transport g_transport_tls =
+{
+ .init = transport_tls_init,
+ .cleanup = transport_tls_cleanup,
+ .try_read = transport_tls_try_read,
+ .try_write = transport_tls_try_write,
+ .get_poll_events = transport_tls_get_poll_events,
+ .in_before_shutdown = transport_tls_in_before_shutdown,
+};
+
+// --- Connection establishment ------------------------------------------------
+
+static bool
+irc_autofill_user_info (struct server *s, struct error **e)
+{
+ const char *nicks = get_config_string (s->config, "nicks");
+ const char *username = get_config_string (s->config, "username");
+ const char *realname = get_config_string (s->config, "realname");
+
+ if (nicks && *nicks && username && *username && realname)
+ return true;
+
+ // Read POSIX user info and fill the configuration if needed
+ errno = 0;
+ struct passwd *pwd = getpwuid (geteuid ());
+ if (!pwd)
+ {
+ return error_set (e,
+ "cannot retrieve user information: %s", strerror (errno));
+ }
+
+ // FIXME: set_config_strings() writes errors on its own
+ if (!nicks || !*nicks)
+ set_config_string (s->config, "nicks", pwd->pw_name);
+ if (!username || !*username)
+ set_config_string (s->config, "username", pwd->pw_name);
+
+ // Not all systems have the GECOS field but the vast majority does
+ if (!realname)
+ {
+ char *gecos = pwd->pw_gecos;
+
+ // The first comma, if any, ends the user's real name
+ char *comma = strchr (gecos, ',');
+ if (comma)
+ *comma = '\0';
+
+ set_config_string (s->config, "realname", gecos);
+ }
+
+ return true;
+}
+
+static char *
+irc_fetch_next_nickname (struct server *s)
+{
+ struct strv v = strv_make ();
+ cstr_split (get_config_string (s->config, "nicks"), ",", true, &v);
+
+ char *result = NULL;
+ if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len)
+ result = xstrdup (v.vector[s->nick_counter++]);
+ if ((size_t) s->nick_counter >= v.len)
+ // Exhausted all nicknames
+ s->nick_counter = -1;
+
+ strv_free (&v);
+ return result;
+}
+
+static void
+irc_register (struct server *s)
+{
+ // Fill in user information automatically if needed
+ irc_autofill_user_info (s, NULL);
+
+ const char *username = get_config_string (s->config, "username");
+ const char *realname = get_config_string (s->config, "realname");
+ hard_assert (username && realname);
+
+ // Start IRCv3 capability negotiation, with up to 3.2 features;
+ // at worst the server will ignore this or send a harmless error message
+ irc_send (s, "CAP LS 302");
+
+ const char *password = get_config_string (s->config, "password");
+ if (password)
+ irc_send (s, "PASS :%s", password);
+
+ s->nick_counter = 0;
+
+ char *nickname = irc_fetch_next_nickname (s);
+ if (nickname)
+ irc_send (s, "NICK :%s", nickname);
+ else
+ log_server_error (s, s->buffer, "No nicks present in configuration");
+ free (nickname);
+
+ // IRC servers may ignore the last argument if it's empty
+ irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " ");
+}
+
+static void
+irc_finish_connection (struct server *s, int socket, const char *hostname)
+{
+ struct app_context *ctx = s->ctx;
+
+ // Most of our output comes from the user one full command at a time and we
+ // use output buffering, so it makes a lot of sense to avoid these delays
+ int yes = 1;
+ soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY,
+ &yes, sizeof yes) != -1);
+
+ set_blocking (socket, false);
+ s->socket = socket;
+ s->transport = get_config_boolean (s->config, "tls")
+ ? &g_transport_tls
+ : &g_transport_plain;
+
+ struct error *e = NULL;
+ if (s->transport->init && !s->transport->init (s, hostname, &e))
+ {
+ log_server_error (s, s->buffer, "Connection failed: #s", e->message);
+ error_free (e);
+
+ xclose (s->socket);
+ s->socket = -1;
+
+ s->transport = NULL;
+ return;
+ }
+
+ log_server_status (s, s->buffer, "Connection established");
+ irc_set_state (s, IRC_CONNECTED);
+
+ s->socket_event = poller_fd_make (&ctx->poller, s->socket);
+ s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready;
+ s->socket_event.user_data = s;
+
+ irc_update_poller (s, NULL);
+ irc_reset_connection_timeouts (s);
+ irc_register (s);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_on_connector_connecting (void *user_data, const char *address)
+{
+ struct server *s = user_data;
+ log_server_status (s, s->buffer, "Connecting to #s...", address);
+}
+
+static void
+irc_on_connector_error (void *user_data, const char *error)
+{
+ struct server *s = user_data;
+ log_server_error (s, s->buffer, "Connection failed: #s", error);
+}
+
+static void
+irc_on_connector_failure (void *user_data)
+{
+ struct server *s = user_data;
+ irc_destroy_connector (s);
+ irc_queue_reconnect (s);
+}
+
+static void
+irc_on_connector_connected (void *user_data, int socket, const char *hostname)
+{
+ struct server *s = user_data;
+ char *hostname_copy = xstrdup (hostname);
+ irc_destroy_connector (s);
+ irc_finish_connection (s, socket, hostname_copy);
+ free (hostname_copy);
+}
+
+static void
+irc_setup_connector (struct server *s, const struct strv *addresses)
+{
+ struct connector *connector = xmalloc (sizeof *connector);
+ connector_init (connector, &s->ctx->poller);
+ s->connector = connector;
+
+ connector->user_data = s;
+ connector->on_connecting = irc_on_connector_connecting;
+ connector->on_error = irc_on_connector_error;
+ connector->on_connected = irc_on_connector_connected;
+ connector->on_failure = irc_on_connector_failure;
+
+ for (size_t i = 0; i < addresses->len; i++)
+ {
+ const char *port = "6667",
+ *host = tokenize_host_port (addresses->vector[i], &port);
+ connector_add_target (connector, host, port);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// TODO: see if we can further merge code for the two connectors, for example
+// by making SOCKS 4A and 5 mere plugins for the connector, or by using
+// a virtual interface common to them both (seems more likely)
+
+static void
+irc_on_socks_connecting (void *user_data,
+ const char *address, const char *via, const char *version)
+{
+ struct server *s = user_data;
+ log_server_status (s, s->buffer,
+ "Connecting to #s via #s (#s)...", address, via, version);
+}
+
+static bool
+irc_setup_connector_socks (struct server *s, const struct strv *addresses,
+ struct error **e)
+{
+ const char *socks_host = get_config_string (s->config, "socks_host");
+ int64_t socks_port_int = get_config_integer (s->config, "socks_port");
+
+ if (!socks_host)
+ return false;
+
+ struct socks_connector *connector = xmalloc (sizeof *connector);
+ socks_connector_init (connector, &s->ctx->poller);
+ s->socks_conn = connector;
+
+ connector->user_data = s;
+ connector->on_connecting = irc_on_socks_connecting;
+ connector->on_error = irc_on_connector_error;
+ connector->on_connected = irc_on_connector_connected;
+ connector->on_failure = irc_on_connector_failure;
+
+ for (size_t i = 0; i < addresses->len; i++)
+ {
+ const char *port = "6667",
+ *host = tokenize_host_port (addresses->vector[i], &port);
+ if (!socks_connector_add_target (connector, host, port, e))
+ return false;
+ }
+
+ char *service = xstrdup_printf ("%" PRIi64, socks_port_int);
+ socks_connector_run (connector, socks_host, service,
+ get_config_string (s->config, "socks_username"),
+ get_config_string (s->config, "socks_password"));
+ free (service);
+
+ // The SOCKS connector can have already failed; we mustn't return true then
+ if (!s->socks_conn)
+ return error_set (e, "SOCKS connection failed");
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_initiate_connect (struct server *s)
+{
+ hard_assert (s->state == IRC_DISCONNECTED);
+
+ const char *addresses = get_config_string (s->config, "addresses");
+ if (!addresses || !addresses[strspn (addresses, ",")])
+ {
+ // No sense in trying to reconnect
+ log_server_error (s, s->buffer,
+ "No addresses specified in configuration");
+ return;
+ }
+
+ struct strv servers = strv_make ();
+ cstr_split (addresses, ",", true, &servers);
+
+ struct error *e = NULL;
+ if (!irc_setup_connector_socks (s, &servers, &e) && !e)
+ irc_setup_connector (s, &servers);
+
+ strv_free (&servers);
+
+ if (e)
+ {
+ irc_destroy_connector (s);
+
+ log_server_error (s, s->buffer, "#s", e->message);
+ error_free (e);
+ irc_queue_reconnect (s);
+ }
+ else if (s->state != IRC_CONNECTED)
+ irc_set_state (s, IRC_CONNECTING);
+}
+
+// --- Input prompt ------------------------------------------------------------
+
+static void
+make_unseen_prefix (struct app_context *ctx, struct str *active_buffers)
+{
+ size_t buffer_no = 0;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ {
+ buffer_no++;
+ if (!(iter->new_messages_count - iter->new_unimportant_count)
+ || iter == ctx->current_buffer)
+ continue;
+
+ if (active_buffers->len)
+ str_append_c (active_buffers, ',');
+ if (iter->highlighted)
+ str_append_c (active_buffers, '!');
+ str_append_printf (active_buffers, "%zu", buffer_no);
+ }
+}
+
+static void
+make_chanmode_postfix (struct channel *channel, struct str *modes)
+{
+ if (channel->no_param_modes.len)
+ str_append (modes, channel->no_param_modes.str);
+
+ struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
+ const char *param;
+ while ((param = str_map_iter_next (&iter)))
+ str_append_c (modes, iter.link->key[0]);
+}
+
+static void
+make_server_postfix_registered (struct buffer *buffer, struct str *output)
+{
+ struct server *s = buffer->server;
+ if (buffer->type == BUFFER_CHANNEL)
+ {
+ struct channel_user *channel_user =
+ irc_channel_get_user (buffer->channel, s->irc_user);
+ if (channel_user)
+ irc_get_channel_user_prefix (s, channel_user, output);
+ }
+ str_append (output, s->irc_user->nickname);
+ if (s->irc_user_modes.len)
+ str_append_printf (output, "(%s)", s->irc_user_modes.str);
+}
+
+static void
+make_server_postfix (struct buffer *buffer, struct str *output)
+{
+ struct server *s = buffer->server;
+ str_append_c (output, ' ');
+ if (!irc_is_connected (s))
+ str_append (output, "(disconnected)");
+ else if (s->state != IRC_REGISTERED)
+ str_append (output, "(unregistered)");
+ else
+ make_server_postfix_registered (buffer, output);
+}
+
+static void
+make_prompt (struct app_context *ctx, struct str *output)
+{
+ LIST_FOR_EACH (struct hook, iter, ctx->prompt_hooks)
+ {
+ struct prompt_hook *hook = (struct prompt_hook *) iter;
+ char *made = hook->make (hook);
+ if (made)
+ {
+ str_append (output, made);
+ free (made);
+ return;
+ }
+ }
+
+ struct buffer *buffer = ctx->current_buffer;
+ if (!buffer)
+ return;
+
+ str_append_c (output, '[');
+
+ struct str active_buffers = str_make ();
+ make_unseen_prefix (ctx, &active_buffers);
+ if (active_buffers.len)
+ str_append_printf (output, "(%s) ", active_buffers.str);
+ str_free (&active_buffers);
+
+ str_append_printf (output, "%d:%s",
+ buffer_get_index (ctx, buffer), buffer->name);
+ // We remember old modes, don't show them while we're not on the channel
+ if (buffer->type == BUFFER_CHANNEL
+ && irc_channel_is_joined (buffer->channel))
+ {
+ struct str modes = str_make ();
+ make_chanmode_postfix (buffer->channel, &modes);
+ if (modes.len)
+ str_append_printf (output, "(+%s)", modes.str);
+ str_free (&modes);
+
+ str_append_printf (output, "{%zu}", buffer->channel->users_len);
+ }
+ if (buffer->hide_unimportant)
+ str_append (output, "<H>");
+
+ if (buffer != ctx->global_buffer)
+ make_server_postfix (buffer, output);
+
+ str_append_c (output, ']');
+ str_append_c (output, ' ');
+}
+
+static void
+input_maybe_set_prompt (struct input *self, char *new_prompt)
+{
+ // Fix libedit's expectations to see a non-control character following
+ // the end mark (see prompt.c and literal.c) by cleaning this up
+ for (char *p = new_prompt; *p; )
+ if (p[0] == INPUT_END_IGNORE && p[1] == INPUT_START_IGNORE)
+ memmove (p, p + 2, strlen (p + 2) + 1);
+ else
+ p++;
+
+ // Redisplay can be an expensive operation
+ const char *prompt = CALL (self, get_prompt);
+ if (prompt && !strcmp (new_prompt, prompt))
+ free (new_prompt);
+ else
+ CALL_ (self, set_prompt, new_prompt);
+}
+
+static void
+on_refresh_prompt (struct app_context *ctx)
+{
+ poller_idle_reset (&ctx->prompt_event);
+ bool have_attributes = !!get_attribute_printer (stdout);
+
+ struct str prompt = str_make ();
+ make_prompt (ctx, &prompt);
+
+ // libedit has a weird bug where it misapplies ignores when they're not
+ // followed by anything else, so let's try to move a trailing space,
+ // which will at least fix the default prompt.
+ const char *attributed_suffix = "";
+#ifdef HAVE_EDITLINE
+ if (have_attributes && prompt.len && prompt.str[prompt.len - 1] == ' ')
+ {
+ prompt.str[--prompt.len] = 0;
+ attributed_suffix = " ";
+ }
+
+ // Also enable a uniform interface for prompt hooks by assuming it uses
+ // GNU Readline escapes: turn this into libedit's almost-flip-flop
+ for (size_t i = 0; i < prompt.len; i++)
+ if (prompt.str[i] == '\x01' || prompt.str[i] == '\x02')
+ prompt.str[i] = INPUT_START_IGNORE /* == INPUT_END_IGNORE */;
+#endif // HAVE_EDITLINE
+
+ char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
+ str_free (&prompt);
+
+ // XXX: to be completely correct, we should use tputs, but we cannot
+ if (have_attributes)
+ {
+ char buf[16384] = "";
+ FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb");
+ struct attr_printer state = { ctx->theme, memfp, false };
+
+ fputc (INPUT_START_IGNORE, memfp);
+ attr_printer_apply_named (&state, ATTR_PROMPT);
+ fputc (INPUT_END_IGNORE, memfp);
+
+ fputs (localized, memfp);
+ free (localized);
+
+ fputc (INPUT_START_IGNORE, memfp);
+ attr_printer_reset (&state);
+ fputc (INPUT_END_IGNORE, memfp);
+
+ fputs (attributed_suffix, memfp);
+
+ fclose (memfp);
+ input_maybe_set_prompt (ctx->input, xstrdup (buf));
+ }
+ else
+ input_maybe_set_prompt (ctx->input, localized);
+}
+
+// --- Helpers -----------------------------------------------------------------
+
+static struct buffer *
+irc_get_buffer_for_message (struct server *s,
+ const struct irc_message *msg, const char *target)
+{
+ // TODO: display such messages differently
+ target = irc_skip_statusmsg (s, target);
+
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
+ if (irc_is_channel (s, target))
+ {
+ struct channel *channel = str_map_find (&s->irc_channels, target);
+ hard_assert (channel || !buffer);
+
+ // This is weird
+ if (!channel)
+ return NULL;
+ }
+ else if (!buffer)
+ {
+ // Outgoing messages needn't have a prefix, no buffer associated
+ if (!msg->prefix)
+ return NULL;
+
+ // Don't make user buffers for servers (they can send NOTICEs)
+ if (!irc_find_userhost (msg->prefix))
+ return s->buffer;
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ if (irc_is_this_us (s, target))
+ buffer = irc_get_or_make_user_buffer (s, nickname);
+ free (nickname);
+
+ // With the IRCv3.2 echo-message capability, we can receive messages
+ // as they are delivered to the target; in that case we return NULL
+ // and the caller should check the origin
+ }
+ return buffer;
+}
+
+static bool
+irc_is_highlight (struct server *s, const char *message)
+{
+ // This may be called by notices before even successfully registering
+ if (!s->irc_user)
+ return false;
+
+ // Strip formatting from the message so that it doesn't interfere
+ // with nickname detection (colour sequences in particular)
+ struct formatter f = formatter_make (s->ctx, NULL);
+ formatter_parse_message (&f, message);
+
+ struct str stripped = str_make ();
+ for (size_t i = 0; i < f.items_len; i++)
+ {
+ if (f.items[i].type == FORMATTER_ITEM_TEXT)
+ str_append (&stripped, f.items[i].text);
+ }
+ formatter_free (&f);
+
+ // Well, this is rather crude but it should make most users happy.
+ // We could do this in proper Unicode but that's two more conversions per
+ // message when both the nickname and the message are likely valid UTF-8.
+ char *copy = str_steal (&stripped);
+ cstr_transform (copy, s->irc_tolower);
+
+ char *nick = xstrdup (s->irc_user->nickname);
+ cstr_transform (nick, s->irc_tolower);
+
+ // Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
+ // Also excluded from the ASCII: common user channel prefixes: +%@&~
+ // XXX: why did I exclude those? It won't match when IRC newbies use them.
+ const char *delimiters = ",.;:!?()<>/=#$* \t\r\n\v\f\"'";
+
+ bool result = false;
+ char *save = NULL;
+ for (char *token = strtok_r (copy, delimiters, &save);
+ token; token = strtok_r (NULL, delimiters, &save))
+ if (!strcmp (token, nick))
+ {
+ result = true;
+ break;
+ }
+
+ free (copy);
+ free (nick);
+ return result;
+}
+
+static char *
+irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
+{
+ struct str prefix = str_make ();
+ if (user && irc_is_channel (s, (target = irc_skip_statusmsg (s, target))))
+ {
+ struct channel *channel;
+ struct channel_user *channel_user;
+ if ((channel = str_map_find (&s->irc_channels, target))
+ && (channel_user = irc_channel_get_user (channel, user)))
+ irc_get_channel_user_prefix (s, channel_user, &prefix);
+ }
+ return str_steal (&prefix);
+}
+
+// --- Mode processor ----------------------------------------------------------
+
+struct mode_processor
+{
+ char **params; ///< Mode string parameters
+ bool adding; ///< Currently adding modes
+ char mode_char; ///< Currently processed mode char
+
+ // User data:
+
+ struct server *s; ///< Server
+ struct channel *channel; ///< The channel being modified
+ unsigned changes; ///< Count of all changes
+ unsigned usermode_changes; ///< Count of all usermode changes
+};
+
+/// Process a single mode character
+typedef bool (*mode_processor_apply_fn) (struct mode_processor *);
+
+static const char *
+mode_processor_next_param (struct mode_processor *self)
+{
+ if (!*self->params)
+ return NULL;
+ return *self->params++;
+}
+
+static void
+mode_processor_run (struct mode_processor *self,
+ char **params, mode_processor_apply_fn apply_cb)
+{
+ self->params = params;
+
+ const char *mode_string;
+ while ((mode_string = mode_processor_next_param (self)))
+ {
+ self->adding = true;
+ while ((self->mode_char = *mode_string++))
+ {
+ if (self->mode_char == '+') self->adding = true;
+ else if (self->mode_char == '-') self->adding = false;
+ else if (!apply_cb (self))
+ break;
+ }
+ }
+}
+
+static int
+mode_char_cmp (const void *a, const void *b)
+{
+ return *(const char *) a - *(const char *) b;
+}
+
+/// Add/remove the current mode character to/from the given ordered list
+static void
+mode_processor_toggle (struct mode_processor *self, struct str *modes)
+{
+ const char *pos = strchr (modes->str, self->mode_char);
+ if (self->adding == !!pos)
+ return;
+
+ if (self->adding)
+ {
+ str_append_c (modes, self->mode_char);
+ qsort (modes->str, modes->len, 1, mode_char_cmp);
+ }
+ else
+ str_remove_slice (modes, pos - modes->str, 1);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+mode_processor_do_user (struct mode_processor *self)
+{
+ const char *nickname;
+ struct user *user;
+ struct channel_user *channel_user;
+ if (!(nickname = mode_processor_next_param (self))
+ || !(user = str_map_find (&self->s->irc_users, nickname))
+ || !(channel_user = irc_channel_get_user (self->channel, user)))
+ return;
+
+ // Translate mode character to user prefix character
+ const char *all_prefixes = self->s->irc_chanuser_prefixes;
+ const char *all_modes = self->s->irc_chanuser_modes;
+
+ const char *mode = strchr (all_modes, self->mode_char);
+ hard_assert (mode && (size_t) (mode - all_modes) < strlen (all_prefixes));
+ char prefix = all_prefixes[mode - all_modes];
+
+ char **prefixes = &channel_user->prefixes;
+ char *pos = strchr (*prefixes, prefix);
+ if (self->adding == !!pos)
+ return;
+
+ if (self->adding)
+ {
+ // Add the new mode prefix while retaining the right order
+ struct str buf = str_make ();
+ for (const char *p = all_prefixes; *p; p++)
+ if (*p == prefix || strchr (*prefixes, *p))
+ str_append_c (&buf, *p);
+ cstr_set (prefixes, str_steal (&buf));
+ }
+ else
+ memmove (pos, pos + 1, strlen (pos));
+}
+
+static void
+mode_processor_do_param_always (struct mode_processor *self)
+{
+ const char *param = NULL;
+ if (!(param = mode_processor_next_param (self)))
+ return;
+
+ char key[2] = { self->mode_char, 0 };
+ str_map_set (&self->channel->param_modes, key,
+ self->adding ? xstrdup (param) : NULL);
+}
+
+static void
+mode_processor_do_param_when_set (struct mode_processor *self)
+{
+ const char *param = NULL;
+ if (self->adding && !(param = mode_processor_next_param (self)))
+ return;
+
+ char key[2] = { self->mode_char, 0 };
+ str_map_set (&self->channel->param_modes, key,
+ self->adding ? xstrdup (param) : NULL);
+}
+
+static bool
+mode_processor_apply_channel (struct mode_processor *self)
+{
+ self->changes++;
+ if (strchr (self->s->irc_chanuser_modes, self->mode_char))
+ {
+ self->usermode_changes++;
+ mode_processor_do_user (self);
+ }
+ else if (strchr (self->s->irc_chanmodes_list, self->mode_char))
+ // Nothing to do here, just skip the next argument if there's any
+ (void) mode_processor_next_param (self);
+ else if (strchr (self->s->irc_chanmodes_param_always, self->mode_char))
+ mode_processor_do_param_always (self);
+ else if (strchr (self->s->irc_chanmodes_param_when_set, self->mode_char))
+ mode_processor_do_param_when_set (self);
+ else if (strchr (self->s->irc_chanmodes_param_never, self->mode_char))
+ mode_processor_toggle (self, &self->channel->no_param_modes);
+ else
+ // It's not safe to continue, results could be undesired
+ return false;
+ return true;
+}
+
+/// Returns whether the change has only affected channel user modes
+static bool
+irc_handle_mode_channel (struct channel *channel, char **params)
+{
+ struct mode_processor p = { .s = channel->s, .channel = channel };
+ mode_processor_run (&p, params, mode_processor_apply_channel);
+ if (p.changes == p.usermode_changes)
+ return true;
+
+ irc_channel_broadcast_buffer_update (channel);
+ return false;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+mode_processor_apply_user (struct mode_processor *self)
+{
+ mode_processor_toggle (self, &self->s->irc_user_modes);
+ return true;
+}
+
+static void
+irc_handle_mode_user (struct server *s, char **params)
+{
+ struct mode_processor p = { .s = s };
+ mode_processor_run (&p, params, mode_processor_apply_user);
+
+ relay_prepare_server_update (s->ctx, s);
+ relay_broadcast (s->ctx);
+}
+
+// --- Output processing -------------------------------------------------------
+
+// Both user and plugins can send whatever the heck they want to,
+// we need to parse it back so that it's evident what's happening
+
+static void
+irc_handle_sent_cap (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 1)
+ return;
+
+ const char *subcommand = msg->params.vector[0];
+ const char *args = (msg->params.len > 1) ? msg->params.vector[1] : "";
+ if (!strcasecmp_ascii (subcommand, "REQ"))
+ log_server_status (s, s->buffer,
+ "#s: #S", "Capabilities requested", args);
+}
+
+static void
+irc_handle_sent_join (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 1)
+ return;
+
+ if (strcmp (msg->params.vector[0], "0"))
+ cstr_split (msg->params.vector[0], ",", true, &s->outstanding_joins);
+}
+
+static void
+irc_handle_sent_notice_text (struct server *s,
+ const struct irc_message *msg, struct str *text)
+{
+ const char *target = msg->params.vector[0];
+ struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
+ if (buffer && soft_assert (s->irc_user))
+ log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str);
+ else
+ log_outcoming_orphan_notice (s, target, text->str);
+}
+
+static void
+irc_handle_sent_notice (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2 || s->cap_echo_message)
+ return;
+
+ // This ignores empty messages which we should not normally send
+ struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
+ LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
+ {
+ if (iter->is_extended)
+ log_ctcp_reply (s, msg->params.vector[0],
+ xstrdup_printf ("%s %s", iter->tag.str, iter->text.str));
+ else
+ irc_handle_sent_notice_text (s, msg, &iter->text);
+ }
+ ctcp_destroy (chunks);
+}
+
+static void
+irc_handle_sent_privmsg_text (struct server *s,
+ const struct irc_message *msg, struct str *text, bool is_action)
+{
+ const char *target = msg->params.vector[0];
+ struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
+ if (buffer && soft_assert (s->irc_user))
+ {
+ char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target);
+ if (is_action)
+ log_outcoming_action (s, buffer, s->irc_user->nickname, text->str);
+ else
+ log_outcoming_privmsg (s, buffer,
+ prefixes, s->irc_user->nickname, text->str);
+ free (prefixes);
+ }
+ else if (is_action)
+ log_outcoming_orphan_action (s, target, text->str);
+ else
+ log_outcoming_orphan_privmsg (s, target, text->str);
+}
+
+static void
+irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2 || s->cap_echo_message)
+ return;
+
+ // This ignores empty messages which we should not normally send
+ // and the server is likely going to reject with an error reply anyway
+ struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
+ LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
+ {
+ if (!iter->is_extended)
+ irc_handle_sent_privmsg_text (s, msg, &iter->text, false);
+ else if (!strcmp (iter->tag.str, "ACTION"))
+ irc_handle_sent_privmsg_text (s, msg, &iter->text, true);
+ else
+ log_ctcp_query (s, msg->params.vector[0], iter->tag.str);
+ }
+ ctcp_destroy (chunks);
+}
+
+static struct irc_handler
+{
+ const char *name;
+ void (*handler) (struct server *s, const struct irc_message *msg);
+}
+g_irc_sent_handlers[] =
+{
+ // This list needs to stay sorted
+ { "CAP", irc_handle_sent_cap },
+ { "JOIN", irc_handle_sent_join },
+ { "NOTICE", irc_handle_sent_notice },
+ { "PRIVMSG", irc_handle_sent_privmsg },
+};
+
+static int
+irc_handler_cmp_by_name (const void *a, const void *b)
+{
+ const struct irc_handler *first = a;
+ const struct irc_handler *second = b;
+ return strcasecmp_ascii (first->name, second->name);
+}
+
+static void
+irc_process_sent_message (const struct irc_message *msg, struct server *s)
+{
+ // The server is free to reject even a matching prefix
+ // XXX: even though no prefix should normally be present, this is racy
+ if (msg->prefix && !irc_is_this_us (s, msg->prefix))
+ return;
+
+ struct irc_handler key = { .name = msg->command };
+ struct irc_handler *handler = bsearch (&key, g_irc_sent_handlers,
+ N_ELEMENTS (g_irc_sent_handlers), sizeof key, irc_handler_cmp_by_name);
+ if (handler)
+ handler->handler (s, msg);
+}
+
+// --- Input handling ----------------------------------------------------------
+
+static void
+irc_handle_authenticate (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 1)
+ return;
+
+ // Empty challenge -> empty response for e.g. SASL EXTERNAL,
+ // abort anything else as it doesn't make much sense to let the user do it
+ if (!strcmp (msg->params.vector[0], "+"))
+ irc_send (s, "AUTHENTICATE +");
+ else
+ irc_send (s, "AUTHENTICATE *");
+}
+
+static void
+irc_handle_away (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix)
+ return;
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ free (nickname);
+
+ // Let's allow the server to make us away
+ if (user)
+ user->away = !!msg->params.len;
+}
+
+static void
+irc_process_cap_ls (struct server *s)
+{
+ log_server_status (s, s->buffer,
+ "#s: #&S", "Capabilities supported", strv_join (&s->cap_ls_buf, " "));
+
+ struct strv chosen = strv_make ();
+ struct strv use = strv_make ();
+
+ cstr_split (get_config_string (s->config, "capabilities"), ",", true, &use);
+
+ // Filter server capabilities for ones we can make use of
+ for (size_t i = 0; i < s->cap_ls_buf.len; i++)
+ {
+ const char *cap = s->cap_ls_buf.vector[i];
+ size_t cap_name_len = strcspn (cap, "=");
+ for (size_t k = 0; k < use.len; k++)
+ if (!strncasecmp_ascii (use.vector[k], cap, cap_name_len))
+ strv_append_owned (&chosen, xstrndup (cap, cap_name_len));
+ }
+ strv_reset (&s->cap_ls_buf);
+
+ char *chosen_str = strv_join (&chosen, " ");
+ strv_free (&chosen);
+ strv_free (&use);
+
+ // XXX: with IRCv3.2, this may end up being too long for one message,
+ // and we need to be careful with CAP END. One probably has to count
+ // the number of sent CAP REQ vs the number of received CAP ACK/NAK.
+ if (s->state == IRC_CONNECTED)
+ irc_send (s, "CAP REQ :%s", chosen_str);
+
+ free (chosen_str);
+}
+
+static void
+irc_toggle_cap (struct server *s, const char *cap, bool active)
+{
+ if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active;
+ if (!strcasecmp_ascii (cap, "away-notify")) s->cap_away_notify = active;
+ if (!strcasecmp_ascii (cap, "sasl")) s->cap_sasl = active;
+}
+
+static void
+irc_try_finish_cap_negotiation (struct server *s)
+{
+ // It does not make sense to do this post-registration, although it would
+ // not hurt either, as the server must ignore it in that case
+ if (s->state == IRC_CONNECTED)
+ irc_send (s, "CAP END");
+}
+
+static void
+irc_handle_cap (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ struct strv v = strv_make ();
+ const char *args = "";
+ if (msg->params.len > 2)
+ cstr_split ((args = msg->params.vector[2]), " ", true, &v);
+
+ const char *subcommand = msg->params.vector[1];
+ if (!strcasecmp_ascii (subcommand, "ACK"))
+ {
+ log_server_status (s, s->buffer,
+ "#s: #S", "Capabilities acknowledged", args);
+ for (size_t i = 0; i < v.len; i++)
+ {
+ const char *cap = v.vector[i];
+ bool active = true;
+ if (*cap == '-')
+ {
+ active = false;
+ cap++;
+ }
+ irc_toggle_cap (s, cap, active);
+ }
+ if (s->cap_sasl && s->transport == &g_transport_tls)
+ irc_send (s, "AUTHENTICATE EXTERNAL");
+ else
+ irc_try_finish_cap_negotiation (s);
+ }
+ else if (!strcasecmp_ascii (subcommand, "NAK"))
+ {
+ log_server_error (s, s->buffer,
+ "#s: #S", "Capabilities not acknowledged", args);
+ irc_try_finish_cap_negotiation (s);
+ }
+ else if (!strcasecmp_ascii (subcommand, "DEL"))
+ {
+ log_server_error (s, s->buffer,
+ "#s: #S", "Capabilities deleted", args);
+ for (size_t i = 0; i < v.len; i++)
+ irc_toggle_cap (s, v.vector[i], false);
+ }
+ else if (!strcasecmp_ascii (subcommand, "LS"))
+ {
+
+ if (msg->params.len > 3 && !strcmp (args, "*"))
+ cstr_split (msg->params.vector[3], " ", true, &s->cap_ls_buf);
+ else
+ {
+ strv_append_vector (&s->cap_ls_buf, v.vector);
+ irc_process_cap_ls (s);
+ }
+ }
+
+ strv_free (&v);
+}
+
+static void
+irc_handle_chghost (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ free (nickname);
+ if (!user)
+ return;
+
+ char *new_prefix = xstrdup_printf ("%s!%s@%s", user->nickname,
+ msg->params.vector[0], msg->params.vector[1]);
+
+ if (irc_is_this_us (s, msg->prefix))
+ {
+ cstr_set (&s->irc_user_host, xstrdup_printf ("%s@%s",
+ msg->params.vector[0], msg->params.vector[1]));
+
+ log_chghost_self (s, s->buffer, new_prefix);
+
+ // Log a message in all open buffers on this server
+ struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
+ struct buffer *buffer;
+ while ((buffer = str_map_iter_next (&iter)))
+ log_chghost_self (s, buffer, new_prefix);
+ }
+ else
+ {
+ // Log a message in any PM buffer
+ struct buffer *buffer =
+ str_map_find (&s->irc_buffer_map, user->nickname);
+ if (buffer)
+ log_chghost (s, buffer, msg->prefix, new_prefix);
+
+ // Log a message in all channels the user is in
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ {
+ buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
+ hard_assert (buffer != NULL);
+ log_chghost (s, buffer, msg->prefix, new_prefix);
+ }
+ }
+
+ free (new_prefix);
+}
+
+static void
+irc_handle_error (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 1)
+ return;
+
+ log_server_error (s, s->buffer, "#m", msg->params.vector[0]);
+}
+
+static void
+irc_handle_invite (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ const char *target = msg->params.vector[0];
+ const char *channel_name = msg->params.vector[1];
+
+ struct buffer *buffer;
+ if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
+ buffer = s->buffer;
+
+ // IRCv3.2 invite-notify extension allows the target to be someone else
+ if (irc_is_this_us (s, target))
+ log_server_status (s, buffer,
+ "#n has invited you to #S", msg->prefix, channel_name);
+ else
+ log_server_status (s, buffer,
+ "#n has invited #n to #S", msg->prefix, target, channel_name);
+}
+
+static bool
+irc_satisfy_join (struct server *s, const char *target)
+{
+ // This queue could use some garbage collection,
+ // but it's unlikely to pose problems.
+ for (size_t i = 0; i < s->outstanding_joins.len; i++)
+ if (!irc_server_strcmp (s, target, s->outstanding_joins.vector[i]))
+ {
+ strv_remove (&s->outstanding_joins, i);
+ return true;
+ }
+ return false;
+}
+
+static void
+irc_handle_join (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 1)
+ return;
+
+ // TODO: RFC 2812 doesn't guarantee that the argument isn't a target list.
+ const char *channel_name = msg->params.vector[0];
+ if (!irc_is_channel (s, channel_name))
+ return;
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ // We've joined a new channel
+ if (!channel)
+ {
+ // This is weird, ignoring
+ if (!irc_is_this_us (s, msg->prefix))
+ return;
+
+ buffer = buffer_new (s->ctx->input,
+ BUFFER_CHANNEL, irc_make_buffer_name (s, channel_name));
+ buffer->server = s;
+ buffer->channel = channel =
+ irc_make_channel (s, xstrdup (channel_name));
+ str_map_set (&s->irc_buffer_map, channel->name, buffer);
+
+ buffer_add (s->ctx, buffer);
+
+ char *input = CALL_ (s->ctx->input, get_line, NULL);
+ if (irc_satisfy_join (s, channel_name) && !*input)
+ buffer_activate (s->ctx, buffer);
+ else
+ buffer->highlighted = true;
+ free (input);
+ }
+
+ if (irc_is_this_us (s, msg->prefix))
+ {
+ // Reset the field so that we rejoin the channel after reconnecting
+ channel->left_manually = false;
+
+ // Request the channel mode as we don't get it automatically
+ str_reset (&channel->no_param_modes);
+ str_map_clear (&channel->param_modes);
+ irc_send (s, "MODE %s", channel_name);
+
+ if ((channel->show_names_after_who = s->cap_away_notify))
+ irc_send (s, "WHO %s", channel_name);
+ }
+
+ // Add the user to the channel
+ char *nickname = irc_cut_nickname (msg->prefix);
+ irc_channel_link_user (channel, irc_get_or_make_user (s, nickname), "");
+ free (nickname);
+
+ // Finally log the message
+ if (buffer)
+ {
+ log_server (s, buffer, BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_JOIN,
+ "#N #a#s#r #S", msg->prefix, ATTR_JOIN, "has joined", channel_name);
+ }
+}
+
+static void
+irc_handle_kick (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ const char *channel_name = msg->params.vector[0];
+ const char *target = msg->params.vector[1];
+ if (!irc_is_channel (s, channel_name)
+ || irc_is_channel (s, target))
+ return;
+
+ const char *message = NULL;
+ if (msg->params.len > 2)
+ message = msg->params.vector[2];
+
+ struct user *user = str_map_find (&s->irc_users, target);
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ // It would be weird for this to be false
+ if (user && channel)
+ {
+ if (irc_is_this_us (s, target))
+ irc_left_channel (channel);
+ else
+ irc_remove_user_from_channel (user, channel);
+ }
+
+ if (buffer)
+ {
+ struct formatter f = formatter_make (s->ctx, s);
+ formatter_add (&f, "#N #a#s#r #n",
+ msg->prefix, ATTR_PART, "has kicked", target);
+ if (message)
+ formatter_add (&f, " (#m)", message);
+ log_formatter (s->ctx, buffer, 0, BUFFER_LINE_PART, &f);
+ }
+}
+
+static void
+irc_handle_kill (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ const char *target = msg->params.vector[0];
+ const char *comment = msg->params.vector[1];
+
+ if (irc_is_this_us (s, target))
+ log_server_status (s, s->buffer,
+ "You've been killed by #n (#m)", msg->prefix, comment);
+}
+
+static void
+irc_handle_mode (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 1)
+ return;
+
+ const char *context = msg->params.vector[0];
+
+ // Join the modes back to a single string
+ struct strv copy = strv_make ();
+ strv_append_vector (&copy, msg->params.vector + 1);
+ char *modes = strv_join (&copy, " ");
+ strv_free (&copy);
+
+ if (irc_is_channel (s, context))
+ {
+ struct channel *channel = str_map_find (&s->irc_channels, context);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, context);
+ hard_assert (channel || !buffer);
+
+ int flags = 0;
+ if (channel
+ && irc_handle_mode_channel (channel, msg->params.vector + 1))
+ // This is 90% automode spam, let's not let it steal attention,
+ // maybe this behaviour should be configurable though
+ flags = BUFFER_LINE_UNIMPORTANT;
+
+ if (buffer)
+ {
+ log_server (s, buffer, flags, BUFFER_LINE_STATUS,
+ "Mode #S [#S] by #n", context, modes, msg->prefix);
+ }
+ }
+ else if (irc_is_this_us (s, context))
+ {
+ irc_handle_mode_user (s, msg->params.vector + 1);
+ log_server_status (s, s->buffer,
+ "User mode [#S] by #n", modes, msg->prefix);
+ }
+
+ free (modes);
+}
+
+static void
+irc_handle_nick (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 1)
+ return;
+
+ const char *new_nickname = msg->params.vector[0];
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ free (nickname);
+ if (!user)
+ return;
+
+ bool lexicographically_different =
+ !!irc_server_strcmp (s, user->nickname, new_nickname);
+
+ // What the fuck, someone renamed themselves to ourselves
+ // TODO: probably log a message and force a reconnect
+ if (lexicographically_different
+ && !irc_server_strcmp (s, new_nickname, s->irc_user->nickname))
+ return;
+
+ // Log a message in any PM buffer (we may even have one for ourselves)
+ struct buffer *pm_buffer =
+ str_map_find (&s->irc_buffer_map, user->nickname);
+ if (pm_buffer)
+ {
+ if (irc_is_this_us (s, msg->prefix))
+ log_nick_self (s, pm_buffer, new_nickname);
+ else
+ log_nick (s, pm_buffer, msg->prefix, new_nickname);
+ }
+
+ // The new nickname may collide with a user referenced by a PM buffer,
+ // or in case of data inconsistency with the server, channels.
+ // In the latter case we need the colliding user to leave all of them.
+ struct user *user_collision = NULL;
+ if (lexicographically_different
+ && (user_collision = str_map_find (&s->irc_users, new_nickname)))
+ LIST_FOR_EACH (struct user_channel, iter, user_collision->channels)
+ irc_remove_user_from_channel (user_collision, iter->channel);
+
+ struct buffer *buffer_collision = NULL;
+ if (lexicographically_different
+ && (buffer_collision = str_map_find (&s->irc_buffer_map, new_nickname)))
+ {
+ hard_assert (buffer_collision->type == BUFFER_PM);
+ hard_assert (buffer_collision->user == user_collision);
+
+ user_unref (buffer_collision->user);
+ buffer_collision->user = user_ref (user);
+ }
+
+ if (pm_buffer && buffer_collision)
+ {
+ // There's not much else we can do other than somehow try to merge
+ // one buffer into the other. In our case, the original buffer wins.
+ buffer_merge (s->ctx, buffer_collision, pm_buffer);
+ if (s->ctx->current_buffer == pm_buffer)
+ buffer_activate (s->ctx, buffer_collision);
+ buffer_remove (s->ctx, pm_buffer);
+ pm_buffer = buffer_collision;
+ }
+
+ // The colliding user should be completely gone by now
+ if (lexicographically_different)
+ hard_assert (!str_map_find (&s->irc_users, new_nickname));
+
+ // Now we can rename everything to reflect the new nickname
+ if (pm_buffer)
+ {
+ str_map_set (&s->irc_buffer_map, user->nickname, NULL);
+ str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);
+
+ char *x = irc_make_buffer_name (s, new_nickname);
+ buffer_rename (s->ctx, pm_buffer, x);
+ free (x);
+ }
+
+ str_map_set (&s->irc_users, user->nickname, NULL);
+ str_map_set (&s->irc_users, new_nickname, user);
+
+ cstr_set (&user->nickname, xstrdup (new_nickname));
+
+ // Finally broadcast the event to relay clients and secondary buffers
+ if (irc_is_this_us (s, new_nickname))
+ {
+ relay_prepare_server_update (s->ctx, s);
+ relay_broadcast (s->ctx);
+
+ log_nick_self (s, s->buffer, new_nickname);
+
+ // Log a message in all open buffers on this server
+ struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
+ struct buffer *buffer;
+ while ((buffer = str_map_iter_next (&iter)))
+ {
+ // We've already done that
+ if (buffer != pm_buffer)
+ log_nick_self (s, buffer, new_nickname);
+ }
+ }
+ else
+ {
+ // Log a message in all channels the user is in
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ {
+ struct buffer *buffer =
+ str_map_find (&s->irc_buffer_map, iter->channel->name);
+ hard_assert (buffer != NULL);
+ log_nick (s, buffer, msg->prefix, new_nickname);
+ }
+ }
+}
+
+static void
+irc_handle_ctcp_reply (struct server *s,
+ const struct irc_message *msg, struct ctcp_chunk *chunk)
+{
+ const char *target = msg->params.vector[0];
+ if (irc_is_this_us (s, msg->prefix))
+ log_ctcp_reply (s, target,
+ xstrdup_printf ("%s %s", chunk->tag.str, chunk->text.str));
+ else
+ log_server_status (s, s->buffer, "CTCP reply from #n: #S #S",
+ msg->prefix, chunk->tag.str, chunk->text.str);
+}
+
+static void
+irc_handle_notice_text (struct server *s,
+ const struct irc_message *msg, struct str *text)
+{
+ const char *target = msg->params.vector[0];
+ struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
+ if (!buffer)
+ {
+ if (irc_is_this_us (s, msg->prefix))
+ log_outcoming_orphan_notice (s, target, text->str);
+ return;
+ }
+
+ char *nick = irc_cut_nickname (msg->prefix);
+ // IRCv3.2 echo-message could otherwise cause us to highlight ourselves
+ if (!irc_is_this_us (s, msg->prefix) && irc_is_highlight (s, text->str))
+ log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, BUFFER_LINE_STATUS,
+ "#a#s(#S)#r: #m", ATTR_HIGHLIGHT, "Notice", nick, text->str);
+ else
+ log_outcoming_notice (s, buffer, msg->prefix, text->str);
+ free (nick);
+}
+
+static void
+irc_handle_notice (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ // This ignores empty messages which we should never receive anyway
+ struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
+ LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
+ if (!iter->is_extended)
+ irc_handle_notice_text (s, msg, &iter->text);
+ else
+ irc_handle_ctcp_reply (s, msg, iter);
+ ctcp_destroy (chunks);
+}
+
+static void
+irc_handle_part (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 1)
+ return;
+
+ const char *channel_name = msg->params.vector[0];
+ if (!irc_is_channel (s, channel_name))
+ return;
+
+ const char *message = NULL;
+ if (msg->params.len > 1)
+ message = msg->params.vector[1];
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ free (nickname);
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ // It would be weird for this to be false
+ if (user && channel)
+ {
+ if (irc_is_this_us (s, msg->prefix))
+ irc_left_channel (channel);
+ else
+ irc_remove_user_from_channel (user, channel);
+ }
+
+ if (buffer)
+ {
+ struct formatter f = formatter_make (s->ctx, s);
+ formatter_add (&f, "#N #a#s#r #S",
+ msg->prefix, ATTR_PART, "has left", channel_name);
+ if (message)
+ formatter_add (&f, " (#m)", message);
+ log_formatter (s->ctx, buffer,
+ BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &f);
+ }
+}
+
+static void
+irc_handle_ping (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len)
+ irc_send (s, "PONG :%s", msg->params.vector[0]);
+ else
+ irc_send (s, "PONG");
+}
+
+static char *
+ctime_now (char buf[26])
+{
+ struct tm tm_;
+ time_t now = time (NULL);
+ if (!asctime_r (localtime_r (&now, &tm_), buf))
+ return NULL;
+
+ // Annoying thing
+ *strchr (buf, '\n') = '\0';
+ return buf;
+}
+
+static void irc_send_ctcp_reply (struct server *s, const char *recipient,
+ const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
+
+static void
+irc_send_ctcp_reply (struct server *s,
+ const char *recipient, const char *format, ...)
+{
+ struct str m = str_make ();
+
+ va_list ap;
+ va_start (ap, format);
+ str_append_vprintf (&m, format, ap);
+ va_end (ap);
+
+ irc_send (s, "NOTICE %s :\x01%s\x01", recipient, m.str);
+ str_free (&m);
+}
+
+static void
+irc_handle_ctcp_request (struct server *s,
+ const struct irc_message *msg, struct ctcp_chunk *chunk)
+{
+ const char *target = msg->params.vector[0];
+ if (irc_is_this_us (s, msg->prefix))
+ {
+ if (s->cap_echo_message)
+ log_ctcp_query (s, target, chunk->tag.str);
+ if (!irc_is_this_us (s, target))
+ return;
+ }
+
+ struct formatter f = formatter_make (s->ctx, s);
+ formatter_add (&f, "CTCP requested by #n", msg->prefix);
+ if (irc_is_channel (s, irc_skip_statusmsg (s, target)))
+ formatter_add (&f, " (to #S)", target);
+ formatter_add (&f, ": #S", chunk->tag.str);
+ log_formatter (s->ctx, s->buffer, 0, BUFFER_LINE_STATUS, &f);
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+
+ if (!strcmp (chunk->tag.str, "CLIENTINFO"))
+ irc_send_ctcp_reply (s, nickname, "CLIENTINFO %s %s %s %s",
+ "PING", "VERSION", "TIME", "CLIENTINFO");
+ else if (!strcmp (chunk->tag.str, "PING"))
+ irc_send_ctcp_reply (s, nickname, "PING %s", chunk->text.str);
+ else if (!strcmp (chunk->tag.str, "VERSION"))
+ {
+ struct utsname info;
+ if (uname (&info))
+ LOG_LIBC_FAILURE ("uname");
+ else
+ irc_send_ctcp_reply (s, nickname, "VERSION %s %s on %s %s",
+ PROGRAM_NAME, PROGRAM_VERSION, info.sysname, info.machine);
+ }
+ else if (!strcmp (chunk->tag.str, "TIME"))
+ {
+ char buf[26];
+ if (!ctime_now (buf))
+ LOG_LIBC_FAILURE ("asctime_r");
+ else
+ irc_send_ctcp_reply (s, nickname, "TIME %s", buf);
+ }
+
+ free (nickname);
+}
+
+static void
+irc_handle_privmsg_text (struct server *s,
+ const struct irc_message *msg, struct str *text, bool is_action)
+{
+ const char *target = msg->params.vector[0];
+ struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
+ if (!buffer)
+ {
+ if (irc_is_this_us (s, msg->prefix))
+ log_outcoming_orphan_privmsg (s, target, text->str);
+ return;
+ }
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ char *prefixes = irc_get_privmsg_prefix
+ (s, str_map_find (&s->irc_users, nickname), target);
+
+ // Make autocomplete offer recent speakers first on partial matches
+ // (note that freshly joined users also move to the front)
+ struct user *user;
+ struct channel_user *channel_user;
+ if (!irc_is_this_us (s, msg->prefix) && buffer->channel
+ && (user = str_map_find (&s->irc_users, nickname))
+ && (channel_user = irc_channel_get_user (buffer->channel, user)))
+ {
+ LIST_UNLINK (buffer->channel->users, channel_user);
+ LIST_PREPEND (buffer->channel->users, channel_user);
+ }
+
+ // IRCv3.2 echo-message could otherwise cause us to highlight ourselves
+ if (irc_is_this_us (s, msg->prefix) || !irc_is_highlight (s, text->str))
+ {
+ if (is_action)
+ log_outcoming_action (s, buffer, nickname, text->str);
+ else
+ log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str);
+ }
+ else if (is_action)
+ log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, BUFFER_LINE_ACTION,
+ "#a#S#r #m", ATTR_HIGHLIGHT, nickname, text->str);
+ else
+ log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, 0,
+ "#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);
+
+ free (nickname);
+ free (prefixes);
+}
+
+static void
+irc_handle_privmsg (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ // This ignores empty messages which we should never receive anyway
+ struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
+ LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
+ if (!iter->is_extended)
+ irc_handle_privmsg_text (s, msg, &iter->text, false);
+ else if (!strcmp (iter->tag.str, "ACTION"))
+ irc_handle_privmsg_text (s, msg, &iter->text, true);
+ else
+ irc_handle_ctcp_request (s, msg, iter);
+ ctcp_destroy (chunks);
+}
+
+static void
+log_quit (struct server *s,
+ struct buffer *buffer, const char *prefix, const char *reason)
+{
+ struct formatter f = formatter_make (s->ctx, s);
+ formatter_add (&f, "#N #a#s#r", prefix, ATTR_PART, "has quit");
+ if (reason)
+ formatter_add (&f, " (#m)", reason);
+ log_formatter (s->ctx, buffer,
+ BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &f);
+}
+
+static void
+irc_handle_quit (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix)
+ return;
+
+ // What the fuck, the server never sends this back
+ if (irc_is_this_us (s, msg->prefix))
+ return;
+
+ char *nickname = irc_cut_nickname (msg->prefix);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+ free (nickname);
+ if (!user)
+ return;
+
+ const char *message = NULL;
+ if (msg->params.len > 0)
+ message = msg->params.vector[0];
+
+ // Log a message in any PM buffer
+ struct buffer *buffer =
+ str_map_find (&s->irc_buffer_map, user->nickname);
+ if (buffer)
+ {
+ log_quit (s, buffer, msg->prefix, message);
+
+ // TODO: set some kind of a flag in the buffer and when the user
+ // reappears on a channel (JOIN), log a "is back online" message.
+ // Also set this flag when we receive a "no such nick" numeric
+ // and reset it when we send something to the buffer.
+ }
+
+ // Log a message in all channels the user is in
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ {
+ if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name)))
+ log_quit (s, buffer, msg->prefix, message);
+
+ // This destroys "iter" which doesn't matter to us
+ irc_remove_user_from_channel (user, iter->channel);
+ }
+}
+
+static void
+irc_handle_tagmsg (struct server *s, const struct irc_message *msg)
+{
+ // TODO: here we can process "typing" tags, once we find this useful
+ (void) s;
+ (void) msg;
+}
+
+static void
+irc_handle_topic (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 2)
+ return;
+
+ const char *channel_name = msg->params.vector[0];
+ const char *topic = msg->params.vector[1];
+ if (!irc_is_channel (s, channel_name))
+ return;
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ // It would be weird for this to be false
+ if (channel)
+ irc_channel_set_topic (channel, topic);
+
+ if (buffer)
+ {
+ log_server (s, buffer, 0, BUFFER_LINE_STATUS, "#n #s \"#m\"",
+ msg->prefix, "has changed the topic to", topic);
+ }
+}
+
+static void
+irc_handle_wallops (struct server *s, const struct irc_message *msg)
+{
+ if (!msg->prefix || msg->params.len < 1)
+ return;
+
+ const char *message = msg->params.vector[0];
+ log_server (s, s->buffer, 0, 0, "<#n> #m", msg->prefix, message);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct irc_handler g_irc_handlers[] =
+{
+ // This list needs to stay sorted
+ { "AUTHENTICATE", irc_handle_authenticate },
+ { "AWAY", irc_handle_away },
+ { "CAP", irc_handle_cap },
+ { "CHGHOST", irc_handle_chghost },
+ { "ERROR", irc_handle_error },
+ { "INVITE", irc_handle_invite },
+ { "JOIN", irc_handle_join },
+ { "KICK", irc_handle_kick },
+ { "KILL", irc_handle_kill },
+ { "MODE", irc_handle_mode },
+ { "NICK", irc_handle_nick },
+ { "NOTICE", irc_handle_notice },
+ { "PART", irc_handle_part },
+ { "PING", irc_handle_ping },
+ { "PRIVMSG", irc_handle_privmsg },
+ { "QUIT", irc_handle_quit },
+ { "TAGMSG", irc_handle_tagmsg },
+ { "TOPIC", irc_handle_topic },
+ { "WALLOPS", irc_handle_wallops },
+};
+
+static bool
+irc_try_parse_word_for_userhost (struct server *s, const char *word)
+{
+ regex_t re;
+ int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
+ if (!soft_assert (!err))
+ return false;
+
+ regmatch_t matches[2];
+ bool result = false;
+ if (!regexec (&re, word, 2, matches, 0))
+ {
+ cstr_set (&s->irc_user_host, xstrndup (word + matches[1].rm_so,
+ matches[1].rm_eo - matches[1].rm_so));
+ result = true;
+ }
+ regfree (&re);
+ return result;
+}
+
+static void
+irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
+{
+ struct strv v = strv_make ();
+ cstr_split (m, " ", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ if (irc_try_parse_word_for_userhost (s, v.vector[i]))
+ break;
+ strv_free (&v);
+}
+
+static bool process_input_line
+ (struct app_context *, struct buffer *, const char *, int);
+static void on_autoaway_timer (struct app_context *ctx);
+
+static void
+irc_on_registered (struct server *s, const char *nickname)
+{
+ s->irc_user = irc_get_or_make_user (s, nickname);
+ str_reset (&s->irc_user_modes);
+ cstr_set (&s->irc_user_host, NULL);
+
+ irc_set_state (s, IRC_REGISTERED);
+
+ // XXX: we can also use WHOIS if it's not supported (optional by RFC 2812)
+ // TODO: maybe rather always use RPL_ISUPPORT NICKLEN & USERLEN & HOSTLEN
+ // since we don't seem to follow any subsequent changes in userhost;
+ // unrealircd sends RPL_HOSTHIDDEN (396), which has an optional user part,
+ // and there is also CAP CHGHOST which /may/ send it to ourselves
+ irc_send (s, "USERHOST %s", s->irc_user->nickname);
+
+ // A little hack that reinstates auto-away status when we get disconnected
+ if (s->autoaway_active)
+ on_autoaway_timer (s->ctx);
+
+ const char *command = get_config_string (s->config, "command");
+ if (command)
+ {
+ log_server_debug (s, "Executing \"#s\"", command);
+ (void) process_input_line (s->ctx, s->buffer, command, 0);
+ }
+
+ int64_t command_delay = get_config_integer (s->config, "command_delay");
+ log_server_debug (s, "Autojoining channels in #&s seconds...",
+ xstrdup_printf ("%" PRId64, command_delay));
+ poller_timer_set (&s->autojoin_tmr, command_delay * 1000);
+}
+
+static void
+irc_handle_rpl_userhost (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ const char *response = msg->params.vector[1];
+ struct strv v = strv_make ();
+ cstr_split (response, " ", true, &v);
+
+ for (size_t i = 0; i < v.len; i++)
+ {
+ char *nick = v.vector[i];
+ char *equals = strchr (nick, '=');
+
+ if (!equals || equals == nick)
+ continue;
+
+ // User is an IRC operator
+ if (equals[-1] == '*')
+ equals[-1] = '\0';
+ else
+ equals[ 0] = '\0';
+
+ // TODO: make use of this (away status polling?)
+ char away_status = equals[1];
+ if (!strchr ("+-", away_status))
+ continue;
+
+ char *userhost = equals + 2;
+ if (irc_is_this_us (s, nick))
+ cstr_set (&s->irc_user_host, xstrdup (userhost));
+ }
+ strv_free (&v);
+}
+
+static void
+irc_handle_rpl_umodeis (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ str_reset (&s->irc_user_modes);
+ irc_handle_mode_user (s, msg->params.vector + 1);
+
+ // XXX: do we want to log a message?
+}
+
+static void
+irc_handle_rpl_namreply (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 4)
+ return;
+
+ const char *channel_name = msg->params.vector[2];
+ const char *nicks = msg->params.vector[3];
+
+ // Just push the nicknames to a string vector to process later
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ if (channel)
+ cstr_split (nicks, " ", true, &channel->names_buf);
+ else
+ log_server_status (s, s->buffer, "Users on #S: #S",
+ channel_name, nicks);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct channel_user_sort_entry
+{
+ struct server *s; ///< Server (because of the qsort API)
+ struct channel_user *channel_user; ///< Channel user
+};
+
+static int
+channel_user_sort_entry_cmp (const void *entry_a, const void *entry_b)
+{
+ const struct channel_user_sort_entry *a = entry_a;
+ const struct channel_user_sort_entry *b = entry_b;
+ struct server *s = a->s;
+
+ // First order by the most significant channel user prefix
+ const char *prio_a = strchr (s->irc_chanuser_prefixes,
+ a->channel_user->prefixes[0]);
+ const char *prio_b = strchr (s->irc_chanuser_prefixes,
+ b->channel_user->prefixes[0]);
+
+ // Put unrecognized prefixes at the end of the list
+ if (prio_a || prio_b)
+ {
+ if (!prio_a) return 1;
+ if (!prio_b) return -1;
+
+ if (prio_a != prio_b)
+ return prio_a - prio_b;
+ }
+
+ return irc_server_strcmp (s,
+ a->channel_user->user->nickname,
+ b->channel_user->user->nickname);
+}
+
+static void
+irc_sort_channel_users (struct channel *channel)
+{
+ size_t n_users = channel->users_len;
+ struct channel_user_sort_entry entries[n_users], *p = entries;
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ {
+ p->s = channel->s;
+ p->channel_user = iter;
+ p++;
+ }
+
+ qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
+
+ channel->users = NULL;
+ while (p-- != entries)
+ LIST_PREPEND (channel->users, p->channel_user);
+}
+
+static char *
+make_channel_users_list (struct channel *channel)
+{
+ size_t n_users = channel->users_len;
+ struct channel_user_sort_entry entries[n_users], *p = entries;
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ {
+ p->s = channel->s;
+ p->channel_user = iter;
+ p++;
+ }
+
+ qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
+
+ // Make names of users that are away italicised, constructing a formatter
+ // and adding a new attribute seems like unnecessary work
+ struct str list = str_make ();
+ for (size_t i = 0; i < n_users; i++)
+ {
+ struct channel_user *channel_user = entries[i].channel_user;
+ if (channel_user->user->away) str_append_c (&list, '\x1d');
+ irc_get_channel_user_prefix (channel->s, channel_user, &list);
+ str_append (&list, channel_user->user->nickname);
+ if (channel_user->user->away) str_append_c (&list, '\x1d');
+ str_append_c (&list, ' ');
+ }
+ if (list.len)
+ list.str[--list.len] = '\0';
+ return str_steal (&list);
+}
+
+static void
+irc_sync_channel_user (struct channel *channel, const char *nickname,
+ const char *prefixes)
+{
+ struct user *user = irc_get_or_make_user (channel->s, nickname);
+ struct channel_user *channel_user =
+ irc_channel_get_user (channel, user);
+ if (!channel_user)
+ {
+ irc_channel_link_user (channel, user, prefixes);
+ return;
+ }
+
+ user_unref (user);
+
+ // If our idea of the user's modes disagrees with what the server's
+ // sent us (the most powerful modes differ), use the latter one
+ if (channel_user->prefixes[0] != prefixes[0])
+ cstr_set (&channel_user->prefixes, xstrdup (prefixes));
+}
+
+static void
+irc_process_names_finish (struct channel *channel)
+{
+ struct server *s = channel->s;
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
+ if (buffer)
+ {
+ log_server_status (channel->s, buffer, "Users on #S: #&m",
+ channel->name, make_channel_users_list (channel));
+ }
+}
+
+static void
+irc_process_names (struct channel *channel)
+{
+ struct str_map present = str_map_make (NULL);
+ present.key_xfrm = channel->s->irc_strxfrm;
+
+ // Either that, or there is no other inhabitant, and sorting does nothing
+ bool we_have_just_joined = channel->users_len == 1;
+
+ struct strv *updates = &channel->names_buf;
+ for (size_t i = 0; i < updates->len; i++)
+ {
+ const char *item = updates->vector[i];
+ size_t n_prefixes = strspn (item, channel->s->irc_chanuser_prefixes);
+ const char *nickname = item + n_prefixes;
+
+ // Store the nickname in a hashset
+ str_map_set (&present, nickname, (void *) 1);
+
+ char *prefixes = xstrndup (item, n_prefixes);
+ irc_sync_channel_user (channel, nickname, prefixes);
+ free (prefixes);
+ }
+
+ // Get rid of channel users missing from "updates"
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ if (!str_map_find (&present, iter->user->nickname))
+ irc_channel_unlink_user (channel, iter);
+
+ str_map_free (&present);
+ strv_reset (&channel->names_buf);
+
+ if (we_have_just_joined)
+ irc_sort_channel_users (channel);
+ if (!channel->show_names_after_who)
+ irc_process_names_finish (channel);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_handle_rpl_endofnames (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ const char *channel_name = msg->params.vector[1];
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ if (!strcmp (channel_name, "*"))
+ {
+ struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
+ struct channel *channel;
+ while ((channel = str_map_iter_next (&iter)))
+ irc_process_names (channel);
+ }
+ else if (channel)
+ irc_process_names (channel);
+}
+
+static bool
+irc_handle_rpl_whoreply (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 7)
+ return false;
+
+ // Sequence: channel, user, host, server, nick, chars
+ const char *channel_name = msg->params.vector[1];
+ const char *nickname = msg->params.vector[5];
+ const char *chars = msg->params.vector[6];
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct user *user = str_map_find (&s->irc_users, nickname);
+
+ // This makes sense to set only with the away-notify capability so far.
+ if (!channel || !channel->show_names_after_who)
+ return false;
+
+ // We track ourselves by other means and we can't track PM-only users yet.
+ if (user && user != s->irc_user && user->channels)
+ user->away = *chars == 'G';
+ return true;
+}
+
+static bool
+irc_handle_rpl_endofwho (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return false;
+
+ const char *target = msg->params.vector[1];
+
+ struct channel *channel = str_map_find (&s->irc_channels, target);
+ if (!channel || !channel->show_names_after_who)
+ return false;
+
+ irc_process_names_finish (channel);
+ channel->show_names_after_who = false;
+ return true;
+}
+
+static void
+irc_handle_rpl_topic (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 3)
+ return;
+
+ const char *channel_name = msg->params.vector[1];
+ const char *topic = msg->params.vector[2];
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ if (channel)
+ irc_channel_set_topic (channel, topic);
+
+ if (buffer)
+ log_server_status (s, buffer, "The topic is: #m", topic);
+}
+
+static void
+irc_handle_rpl_channelmodeis (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ const char *channel_name = msg->params.vector[1];
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ if (channel)
+ {
+ str_reset (&channel->no_param_modes);
+ str_map_clear (&channel->param_modes);
+
+ irc_handle_mode_channel (channel, msg->params.vector + 1);
+ }
+
+ // XXX: do we want to log a message?
+}
+
+static char *
+make_time_string (time_t time)
+{
+ char buf[32];
+ struct tm tm;
+ strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&time, &tm));
+ return xstrdup (buf);
+}
+
+static void
+irc_handle_rpl_creationtime (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 3)
+ return;
+
+ const char *channel_name = msg->params.vector[1];
+ const char *creation_time = msg->params.vector[2];
+
+ unsigned long created;
+ if (!xstrtoul (&created, creation_time, 10))
+ return;
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ if (buffer)
+ {
+ log_server_status (s, buffer, "Channel created on #&s",
+ make_time_string (created));
+ }
+}
+
+static void
+irc_handle_rpl_topicwhotime (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 4)
+ return;
+
+ const char *channel_name = msg->params.vector[1];
+ const char *who = msg->params.vector[2];
+ const char *change_time = msg->params.vector[3];
+
+ unsigned long changed;
+ if (!xstrtoul (&changed, change_time, 10))
+ return;
+
+ struct channel *channel = str_map_find (&s->irc_channels, channel_name);
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
+ hard_assert (channel || !buffer);
+
+ if (buffer)
+ {
+ log_server_status (s, buffer, "Topic set by #N on #&s",
+ who, make_time_string (changed));
+ }
+}
+
+static void
+irc_handle_rpl_inviting (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 3)
+ return;
+
+ const char *nickname = msg->params.vector[1];
+ const char *channel_name = msg->params.vector[2];
+
+ struct buffer *buffer;
+ if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
+ buffer = s->buffer;
+
+ log_server_status (s, buffer,
+ "You have invited #n to #S", nickname, channel_name);
+}
+
+static void
+irc_handle_err_nicknameinuse (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ log_server_error (s, s->buffer,
+ "Nickname is already in use: #S", msg->params.vector[1]);
+
+ // Only do this while we haven't successfully registered yet
+ if (s->state != IRC_CONNECTED)
+ return;
+
+ char *nickname = irc_fetch_next_nickname (s);
+ if (nickname)
+ {
+ log_server_status (s, s->buffer, "Retrying with #s...", nickname);
+ irc_send (s, "NICK :%s", nickname);
+ free (nickname);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_handle_isupport_prefix (struct server *s, char *value)
+{
+ char *modes = value;
+ char *prefixes = strchr (value, ')');
+ size_t n_prefixes = prefixes - modes;
+ if (*modes++ != '(' || !prefixes++ || strlen (value) != 2 * n_prefixes--)
+ return;
+
+ cstr_set (&s->irc_chanuser_modes, xstrndup (modes, n_prefixes));
+ cstr_set (&s->irc_chanuser_prefixes, xstrndup (prefixes, n_prefixes));
+}
+
+static void
+irc_handle_isupport_casemapping (struct server *s, char *value)
+{
+ if (!strcmp (value, "ascii"))
+ irc_set_casemapping (s, tolower_ascii, tolower_ascii_strxfrm);
+ else if (!strcmp (value, "rfc1459"))
+ irc_set_casemapping (s, irc_tolower, irc_strxfrm);
+ else if (!strcmp (value, "rfc1459-strict"))
+ irc_set_casemapping (s, irc_tolower_strict, irc_strxfrm_strict);
+}
+
+static void
+irc_handle_isupport_chantypes (struct server *s, char *value)
+{
+ cstr_set (&s->irc_chantypes, xstrdup (value));
+}
+
+static void
+irc_handle_isupport_idchan (struct server *s, char *value)
+{
+ struct str prefixes = str_make ();
+ struct strv v = strv_make ();
+ cstr_split (value, ",", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ {
+ // Not using or validating the numeric part
+ const char *pair = v.vector[i];
+ const char *colon = strchr (pair, ':');
+ if (colon)
+ str_append_data (&prefixes, pair, colon - pair);
+ }
+ strv_free (&v);
+ cstr_set (&s->irc_idchan_prefixes, str_steal (&prefixes));
+}
+
+static void
+irc_handle_isupport_statusmsg (struct server *s, char *value)
+{
+ cstr_set (&s->irc_statusmsg, xstrdup (value));
+}
+
+static void
+irc_handle_isupport_extban (struct server *s, char *value)
+{
+ s->irc_extban_prefix = 0;
+ if (*value && *value != ',')
+ s->irc_extban_prefix = *value++;
+
+ cstr_set (&s->irc_extban_types, xstrdup (*value == ',' ? ++value : ""));
+}
+
+static void
+irc_handle_isupport_chanmodes (struct server *s, char *value)
+{
+ struct strv v = strv_make ();
+ cstr_split (value, ",", true, &v);
+ if (v.len >= 4)
+ {
+ cstr_set (&s->irc_chanmodes_list, xstrdup (v.vector[0]));
+ cstr_set (&s->irc_chanmodes_param_always, xstrdup (v.vector[1]));
+ cstr_set (&s->irc_chanmodes_param_when_set, xstrdup (v.vector[2]));
+ cstr_set (&s->irc_chanmodes_param_never, xstrdup (v.vector[3]));
+ }
+ strv_free (&v);
+}
+
+static void
+irc_handle_isupport_modes (struct server *s, char *value)
+{
+ unsigned long modes;
+ if (!*value)
+ s->irc_max_modes = UINT_MAX;
+ else if (xstrtoul (&modes, value, 10) && modes && modes <= UINT_MAX)
+ s->irc_max_modes = modes;
+}
+
+static void
+unescape_isupport_value (const char *value, struct str *output)
+{
+ const char *alphabet = "0123456789abcdef", *a, *b;
+ for (const char *p = value; *p; p++)
+ {
+ if (p[0] == '\\'
+ && p[1] == 'x'
+ && p[2] && (a = strchr (alphabet, tolower_ascii (p[2])))
+ && p[3] && (b = strchr (alphabet, tolower_ascii (p[3]))))
+ {
+ str_append_c (output, (a - alphabet) << 4 | (b - alphabet));
+ p += 3;
+ }
+ else
+ str_append_c (output, *p);
+ }
+}
+
+static void
+dispatch_isupport (struct server *s, const char *name, char *value)
+{
+#define MATCH(from, to) if (!strcmp (name, (from))) { (to) (s, value); return; }
+
+ // TODO: also make use of TARGMAX to split client commands as necessary
+
+ MATCH ("PREFIX", irc_handle_isupport_prefix);
+ MATCH ("CASEMAPPING", irc_handle_isupport_casemapping);
+ MATCH ("CHANTYPES", irc_handle_isupport_chantypes);
+ MATCH ("IDCHAN", irc_handle_isupport_idchan);
+ MATCH ("STATUSMSG", irc_handle_isupport_statusmsg);
+ MATCH ("EXTBAN", irc_handle_isupport_extban);
+ MATCH ("CHANMODES", irc_handle_isupport_chanmodes);
+ MATCH ("MODES", irc_handle_isupport_modes);
+
+#undef MATCH
+}
+
+static void
+irc_handle_rpl_isupport (struct server *s, const struct irc_message *msg)
+{
+ if (msg->params.len < 2)
+ return;
+
+ for (size_t i = 1; i < msg->params.len - 1; i++)
+ {
+ // TODO: if the parameter starts with "-", it resets to default
+ char *param = msg->params.vector[i];
+ char *value = param + strcspn (param, "=");
+ if (*value) *value++ = '\0';
+
+ struct str value_unescaped = str_make ();
+ unescape_isupport_value (value, &value_unescaped);
+ dispatch_isupport (s, param, value_unescaped.str);
+ str_free (&value_unescaped);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_adjust_motd (char **motd)
+{
+ // Heuristic, force MOTD to be monospace in graphical frontends.
+ if (!strchr (*motd, '\x11'))
+ {
+ struct str s = str_make ();
+ str_append_c (&s, '\x11');
+ for (const char *p = *motd; *p; p++)
+ {
+ str_append_c (&s, *p);
+ if (*p == '\x0f')
+ str_append_c (&s, '\x11');
+ }
+ cstr_set (motd, str_steal (&s));
+ }
+}
+
+static void
+irc_process_numeric (struct server *s,
+ const struct irc_message *msg, unsigned long numeric)
+{
+ // Numerics typically have human-readable information
+
+ // Get rid of the first parameter, if there's any at all,
+ // as it contains our nickname and is of no practical use to the user
+ struct strv copy = strv_make ();
+ strv_append_vector (&copy, msg->params.vector + !!msg->params.len);
+
+ struct buffer *buffer = s->buffer;
+ int flags = 0;
+ switch (numeric)
+ {
+ case IRC_RPL_WELCOME:
+ irc_on_registered (s, msg->params.vector[0]);
+
+ // We still issue a USERHOST anyway as this is in general unreliable
+ if (msg->params.len == 2)
+ irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]);
+ break;
+ case IRC_RPL_MOTDSTART:
+ case IRC_RPL_MOTD:
+ if (copy.len)
+ irc_adjust_motd (&copy.vector[0]);
+ break;
+
+ case IRC_RPL_ISUPPORT:
+ irc_handle_rpl_isupport (s, msg); break;
+ case IRC_RPL_USERHOST:
+ irc_handle_rpl_userhost (s, msg); break;
+ case IRC_RPL_UMODEIS:
+ irc_handle_rpl_umodeis (s, msg); buffer = NULL; break;
+ case IRC_RPL_NAMREPLY:
+ irc_handle_rpl_namreply (s, msg); buffer = NULL; break;
+ case IRC_RPL_ENDOFNAMES:
+ irc_handle_rpl_endofnames (s, msg); buffer = NULL; break;
+ case IRC_RPL_TOPIC:
+ irc_handle_rpl_topic (s, msg); buffer = NULL; break;
+ case IRC_RPL_CHANNELMODEIS:
+ irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break;
+ case IRC_RPL_CREATIONTIME:
+ irc_handle_rpl_creationtime (s, msg); buffer = NULL; break;
+ case IRC_RPL_TOPICWHOTIME:
+ irc_handle_rpl_topicwhotime (s, msg); buffer = NULL; break;
+ case IRC_RPL_INVITING:
+ irc_handle_rpl_inviting (s, msg); buffer = NULL; break;
+
+ case IRC_ERR_NICKNAMEINUSE:
+ irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break;
+
+ // Auto-away spams server buffers with activity
+ case IRC_RPL_NOWAWAY:
+ flags |= BUFFER_LINE_UNIMPORTANT;
+ if (s->irc_user) s->irc_user->away = true;
+ break;
+ case IRC_RPL_UNAWAY:
+ flags |= BUFFER_LINE_UNIMPORTANT;
+ if (s->irc_user) s->irc_user->away = false;
+ break;
+
+ case IRC_RPL_WHOREPLY:
+ if (irc_handle_rpl_whoreply (s, msg)) buffer = NULL;
+ break;
+ case IRC_RPL_ENDOFWHO:
+ if (irc_handle_rpl_endofwho (s, msg)) buffer = NULL;
+ break;
+
+ case IRC_ERR_NICKLOCKED:
+ case IRC_RPL_SASLSUCCESS:
+ case IRC_ERR_SASLFAIL:
+ case IRC_ERR_SASLTOOLONG:
+ case IRC_ERR_SASLABORTED:
+ case IRC_ERR_SASLALREADY:
+ irc_try_finish_cap_negotiation (s);
+ break;
+
+ case IRC_RPL_LIST:
+
+ case IRC_ERR_UNKNOWNCOMMAND:
+ case IRC_ERR_NEEDMOREPARAMS:
+ // Just preventing these commands from getting printed in a more
+ // specific buffer as that would be unwanted
+ break;
+
+ default:
+ // If the second parameter is something we have a buffer for
+ // (a channel, a PM buffer), log it in that buffer. This is very basic.
+ // TODO: whitelist/blacklist a lot more replies in here.
+ // TODO: we should either strip the first parameter from the resulting
+ // buffer line, or at least put it in brackets
+ if (msg->params.len < 2)
+ break;
+
+ struct buffer *x;
+ if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
+ buffer = x;
+
+ // A JOIN request should be split at commas,
+ // then for each element produce either a JOIN response, or a numeric.
+ (void) irc_satisfy_join (s, msg->params.vector[1]);
+ }
+
+ if (buffer)
+ {
+ // Join the parameter vector back and send it to the server buffer
+ log_server (s, buffer, flags, BUFFER_LINE_STATUS,
+ "#&m", strv_join (&copy, " "));
+ }
+
+ strv_free (&copy);
+}
+
+static void
+irc_sanitize_cut_off_utf8 (char **line)
+{
+ // A variation on utf8_validate(), we need to detect the -2 return
+ const char *p = *line, *end = strchr (p, 0);
+ int32_t codepoint;
+ while ((codepoint = utf8_decode (&p, end - p)) >= 0
+ && utf8_validate_cp (codepoint))
+ ;
+ if (codepoint != -2)
+ return;
+
+ struct str fixed_up = str_make ();
+ str_append_data (&fixed_up, *line, p - *line);
+ str_append (&fixed_up, "\xEF\xBF\xBD" /* U+FFFD */);
+ cstr_set (line, str_steal (&fixed_up));
+}
+
+static void
+irc_process_message (const struct irc_message *msg, struct server *s)
+{
+ if (msg->params.len)
+ irc_sanitize_cut_off_utf8 (&msg->params.vector[msg->params.len - 1]);
+
+ // TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec())
+ // -> change all calls to log_{server,nick,chghost,outcoming,ctcp}*()
+ // to take an extra numeric argument specifying time
+ struct irc_handler key = { .name = msg->command };
+ struct irc_handler *handler = bsearch (&key, g_irc_handlers,
+ N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name);
+ if (handler)
+ handler->handler (s, msg);
+
+ unsigned long numeric;
+ if (xstrtoul (&numeric, msg->command, 10))
+ irc_process_numeric (s, msg, numeric);
+
+ // Better always make sure everything is in sync rather than care about
+ // each case explicitly whether anything might have changed
+ refresh_prompt (s->ctx);
+}
+
+// --- Message autosplitting magic ---------------------------------------------
+
+// This is a rather basic algorithm; something like ICU with proper
+// locale specification would be needed to make it work better.
+
+static size_t
+wrap_text_for_single_line (const char *text, struct irc_char_attrs *attrs,
+ size_t text_len, size_t target_len, struct str *output)
+{
+ size_t eaten = 0;
+
+ // First try going word by word
+ const char *word_start;
+ const char *word_end = text + strcspn (text, " ");
+ size_t word_len = word_end - text;
+ while (target_len && word_len <= target_len)
+ {
+ if (word_len)
+ {
+ str_append_data (output, text, word_len);
+
+ text += word_len;
+ eaten += word_len;
+ target_len -= word_len;
+ }
+
+ // Find the next word's end
+ word_start = text + strspn (text, " ");
+ word_end = word_start + strcspn (word_start, " ");
+ word_len = word_end - text;
+ }
+
+ if (eaten)
+ // Discard whitespace between words if split
+ return eaten + (word_start - text);
+
+ // And if that doesn't help, cut the longest valid block of characters
+ for (size_t i = 1; i <= text_len && i <= target_len; i++)
+ if (i == text_len || attrs[i].starts_at_boundary)
+ eaten = i;
+
+ str_append_data (output, text, eaten);
+ return eaten;
+}
+
+// In practice, this should never fail at all, although it's not guaranteed
+static bool
+wrap_message (const char *message,
+ int line_max, struct strv *output, struct error **e)
+{
+ size_t message_left = strlen (message), i = 0;
+ struct irc_char_attrs *attrs = irc_analyze_text (message, message_left);
+ struct str m = str_make ();
+ if (line_max <= 0)
+ goto error;
+
+ while (m.len + message_left > (size_t) line_max)
+ {
+ size_t eaten = wrap_text_for_single_line
+ (message + i, attrs + i, message_left, line_max - m.len, &m);
+ if (!eaten)
+ goto error;
+
+ strv_append_owned (output, str_steal (&m));
+ m = str_make ();
+
+ i += eaten;
+ if (!(message_left -= eaten))
+ break;
+
+ irc_serialize_char_attrs (attrs + i, &m);
+ if (m.len >= (size_t) line_max)
+ {
+ print_debug ("formatting continuation too long");
+ str_reset (&m);
+ }
+ }
+ if (message_left)
+ strv_append_owned (output,
+ xstrdup_printf ("%s%s", m.str, message + i));
+
+ free (attrs);
+ str_free (&m);
+ return true;
+
+error:
+ free (attrs);
+ str_free (&m);
+ return error_set (e,
+ "Message splitting was unsuccessful as there was "
+ "too little room for UTF-8 characters");
+}
+
+/// Automatically splits messages that arrive at other clients with our prefix
+/// so that they don't arrive cut off by the server
+static bool
+irc_autosplit_message (struct server *s, const char *message,
+ int fixed_part, struct strv *output, struct error **e)
+{
+ // :<nick>!<user>@<host> <fixed-part><message>
+ int space_in_one_message = 0;
+ if (s->irc_user && s->irc_user_host)
+ space_in_one_message = 510
+ - 1 - (int) strlen (s->irc_user->nickname)
+ - 1 - (int) strlen (s->irc_user_host)
+ - 1 - fixed_part;
+
+ // Multiline messages can be triggered through hooks and plugins.
+ struct strv lines = strv_make ();
+ cstr_split (message, "\r\n", false, &lines);
+ bool success = true;
+ for (size_t i = 0; i < lines.len; i++)
+ {
+ // We don't always have the full info for message splitting.
+ if (!space_in_one_message)
+ strv_append (output, lines.vector[i]);
+ else if (!(success =
+ wrap_message (lines.vector[i], space_in_one_message, output, e)))
+ break;
+ }
+ strv_free (&lines);
+ return success;
+}
+
+static void
+send_autosplit_message (struct server *s,
+ const char *command, const char *target, const char *message,
+ const char *prefix, const char *suffix)
+{
+ struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
+
+ // "COMMAND target * :prefix*suffix"
+ int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
+ + strlen (prefix) + strlen (suffix);
+
+ struct strv lines = strv_make ();
+ struct error *e = NULL;
+ if (!irc_autosplit_message (s, message, fixed_part, &lines, &e))
+ {
+ log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message);
+ error_free (e);
+ }
+ else
+ {
+ for (size_t i = 0; i < lines.len; i++)
+ irc_send (s, "%s %s :%s%s%s", command, target,
+ prefix, lines.vector[i], suffix);
+ }
+ strv_free (&lines);
+}
+
+#define SEND_AUTOSPLIT_ACTION(s, target, message) \
+ send_autosplit_message ((s), "PRIVMSG", (target), (message), \
+ "\x01" "ACTION ", "\x01")
+
+#define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \
+ send_autosplit_message ((s), "PRIVMSG", (target), (message), "", "")
+
+#define SEND_AUTOSPLIT_NOTICE(s, target, message) \
+ send_autosplit_message ((s), "NOTICE", (target), (message), "", "")
+
+// --- Configuration dumper ----------------------------------------------------
+
+struct config_dump_data
+{
+ struct strv path; ///< Levels
+ struct strv *output; ///< Where to place new entries
+};
+
+static void config_dump_item
+ (struct config_item *item, struct config_dump_data *data);
+
+static void
+config_dump_children
+ (struct config_item *object, struct config_dump_data *data)
+{
+ hard_assert (object->type == CONFIG_ITEM_OBJECT);
+
+ struct str_map_iter iter = str_map_iter_make (&object->value.object);
+ struct config_item *child;
+ while ((child = str_map_iter_next (&iter)))
+ {
+ strv_append_owned (&data->path, iter.link->key);
+ config_dump_item (child, data);
+ strv_steal (&data->path, data->path.len - 1);
+ }
+}
+
+static void
+config_dump_item (struct config_item *item, struct config_dump_data *data)
+{
+ // Empty objects will show as such
+ if (item->type == CONFIG_ITEM_OBJECT
+ && item->value.object.len)
+ {
+ config_dump_children (item, data);
+ return;
+ }
+
+ // Currently there's no reason for us to dump unknown items
+ struct config_schema *schema = item->schema;
+ if (!schema)
+ return;
+
+ struct str line = str_make ();
+ if (data->path.len)
+ str_append (&line, data->path.vector[0]);
+ for (size_t i = 1; i < data->path.len; i++)
+ str_append_printf (&line, ".%s", data->path.vector[i]);
+
+ struct str value = str_make ();
+ config_item_write (item, false, &value);
+
+ // Don't bother writing out null values everywhere
+ bool has_default = schema && schema->default_;
+ if (item->type != CONFIG_ITEM_NULL || has_default)
+ {
+ str_append (&line, " = ");
+ str_append_str (&line, &value);
+ }
+
+ if (!schema)
+ str_append (&line, " (unrecognized)");
+ else if (has_default && strcmp (schema->default_, value.str))
+ str_append_printf (&line, " (default: %s)", schema->default_);
+ else if (!has_default && item->type != CONFIG_ITEM_NULL)
+ str_append_printf (&line, " (default: %s)", "null");
+
+ str_free (&value);
+ strv_append_owned (data->output, str_steal (&line));
+}
+
+static void
+config_dump (struct config_item *root, struct strv *output)
+{
+ struct config_dump_data data;
+ data.path = strv_make ();
+ data.output = output;
+
+ config_dump_item (root, &data);
+
+ hard_assert (!data.path.len);
+ strv_free (&data.path);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+strv_sort_cb (const void *a, const void *b)
+{
+ return strcmp (*(const char **) a, *(const char **) b);
+}
+
+static void
+strv_sort (struct strv *self)
+{
+ qsort (self->vector, self->len, sizeof *self->vector, strv_sort_cb);
+}
+
+static void
+dump_matching_options
+ (struct config_item *root, const char *mask, struct strv *output)
+{
+ config_dump (root, output);
+ strv_sort (output);
+
+ // Filter out results by wildcard matching
+ for (size_t i = 0; i < output->len; i++)
+ {
+ // Yeah, I know
+ char *key = cstr_cut_until (output->vector[i], " ");
+ if (fnmatch (mask, key, 0))
+ strv_remove (output, i--);
+ free (key);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+save_configuration (struct app_context *ctx)
+{
+ struct str data = str_make ();
+ serialize_configuration (ctx->config.root, &data);
+
+ struct error *e = NULL;
+ char *filename = write_configuration_file (NULL, &data, &e);
+ str_free (&data);
+
+ if (!filename)
+ {
+ log_global_error (ctx,
+ "#s: #s", "Saving configuration failed", e->message);
+ error_free (e);
+ }
+ else
+ log_global_status (ctx, "Configuration written to `#s'", filename);
+ free (filename);
+}
+
+// --- Server management -------------------------------------------------------
+
+static bool
+validate_server_name (const char *name)
+{
+ for (const unsigned char *p = (const unsigned char *) name; *p; p++)
+ if (iscntrl_ascii (*p) || *p == '.')
+ return false;
+ return true;
+}
+
+static const char *
+check_server_name_for_addition (struct app_context *ctx, const char *name)
+{
+ if (!strcasecmp_ascii (name, ctx->global_buffer->name))
+ return "name collides with the global buffer";
+ if (str_map_find (&ctx->servers, name))
+ return "server already exists";
+ if (!validate_server_name (name))
+ return "invalid server name";
+ return NULL;
+}
+
+static struct server *
+server_add (struct app_context *ctx,
+ const char *name, struct config_item *subtree)
+{
+ hard_assert (!str_map_find (&ctx->servers, name));
+
+ struct server *s = server_new (&ctx->poller);
+ s->ctx = ctx;
+ s->name = xstrdup (name);
+ str_map_set (&ctx->servers, s->name, s);
+ s->config = subtree;
+
+ // Add a buffer and activate it
+ struct buffer *buffer = s->buffer = buffer_new (ctx->input,
+ BUFFER_SERVER, irc_make_buffer_name (s, NULL));
+ buffer->server = s;
+
+ buffer_add (ctx, buffer);
+ buffer_activate (ctx, buffer);
+
+ config_schema_apply_to_object (g_config_server, subtree, s);
+ config_schema_call_changed (subtree);
+
+ if (get_config_boolean (s->config, "autoconnect"))
+ // Connect to the server ASAP
+ poller_timer_set (&s->reconnect_tmr, 0);
+ return s;
+}
+
+static void
+server_add_new (struct app_context *ctx, const char *name)
+{
+ // Note that there may already be something in the configuration under
+ // that key that we've ignored earlier, and there may also be
+ // a case-insensitive conflict. Those things may only happen as a result
+ // of manual edits to the configuration, though, and they're not really
+ // going to break anything. They only cause surprises when loading.
+ struct str_map *servers = get_servers_config (ctx);
+ struct config_item *subtree = config_item_object ();
+ str_map_set (servers, name, subtree);
+
+ struct server *s = server_add (ctx, name, subtree);
+ struct error *e = NULL;
+ if (!irc_autofill_user_info (s, &e))
+ {
+ log_server_error (s, s->buffer,
+ "#s: #s", "Failed to fill in user details", e->message);
+ error_free (e);
+ }
+}
+
+static void
+server_remove (struct app_context *ctx, struct server *s)
+{
+ hard_assert (!irc_is_connected (s));
+
+ if (s->buffer)
+ buffer_remove_safe (ctx, s->buffer);
+
+ relay_prepare_server_remove (ctx, s);
+ relay_broadcast (ctx);
+
+ struct str_map_unset_iter iter =
+ str_map_unset_iter_make (&s->irc_buffer_map);
+ struct buffer *buffer;
+ while ((buffer = str_map_unset_iter_next (&iter)))
+ buffer_remove_safe (ctx, buffer);
+ str_map_unset_iter_free (&iter);
+
+ hard_assert (!s->buffer);
+ hard_assert (!s->irc_buffer_map.len);
+ hard_assert (!s->irc_channels.len);
+ soft_assert (!s->irc_users.len);
+
+ str_map_set (get_servers_config (ctx), s->name, NULL);
+ s->config = NULL;
+
+ // This actually destroys the server as it's owned by the map
+ str_map_set (&ctx->servers, s->name, NULL);
+}
+
+static void
+server_rename (struct app_context *ctx, struct server *s, const char *new_name)
+{
+ hard_assert (!str_map_find (&ctx->servers, new_name));
+
+ relay_prepare_server_rename (ctx, s, new_name);
+ relay_broadcast (ctx);
+
+ str_map_set (&ctx->servers, new_name,
+ str_map_steal (&ctx->servers, s->name));
+
+ struct str_map *servers = get_servers_config (ctx);
+ str_map_set (servers, new_name, str_map_steal (servers, s->name));
+
+ cstr_set (&s->name, xstrdup (new_name));
+ buffer_rename (ctx, s->buffer, new_name);
+
+ struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
+ struct buffer *buffer;
+ while ((buffer = str_map_iter_next (&iter)))
+ {
+ char *x = NULL;
+ switch (buffer->type)
+ {
+ case BUFFER_PM:
+ x = irc_make_buffer_name (s, buffer->user->nickname);
+ break;
+ case BUFFER_CHANNEL:
+ x = irc_make_buffer_name (s, buffer->channel->name);
+ break;
+ default:
+ hard_assert (!"unexpected type of server-related buffer");
+ }
+ buffer_rename (ctx, buffer, x);
+ free (x);
+ }
+}
+
+// --- Plugins -----------------------------------------------------------------
+
+/// Returns the basename of the plugin's name without any extensions,
+/// or NULL if the name isn't suitable (starts with a dot)
+static char *
+plugin_config_name (struct plugin *self)
+{
+ const char *begin = self->name;
+ for (const char *p = begin; *p; )
+ if (*p++ == '/')
+ begin = p;
+
+ size_t len = strcspn (begin, ".");
+ if (!len)
+ return NULL;
+
+ // XXX: we might also allow arbitrary strings as object keys (except dots)
+ char *copy = xstrndup (begin, len);
+ for (char *p = copy; *p; p++)
+ if (!config_tokenizer_is_word_char (*p))
+ *p = '_';
+ return copy;
+}
+
+// --- Lua ---------------------------------------------------------------------
+
+// Each plugin has its own Lua state object, so that a/ they don't disturb each
+// other and b/ unloading a plugin releases all resources.
+//
+// References to internal objects (buffers, servers) are all weak.
+
+#ifdef HAVE_LUA
+
+struct lua_plugin
+{
+ struct plugin super; ///< The structure we're deriving
+ struct app_context *ctx; ///< Application context
+ lua_State *L; ///< Lua state for the main thread
+
+ struct lua_schema_item *schemas; ///< Registered schema items
+};
+
+static void
+lua_plugin_gc (struct plugin *self_)
+{
+ struct lua_plugin *self = (struct lua_plugin *) self_;
+ lua_gc (self->L, LUA_GCCOLLECT, 0 /* Lua 5.3 required, 5.4 varargs */);
+}
+
+static void
+lua_plugin_free (struct plugin *self_)
+{
+ struct lua_plugin *self = (struct lua_plugin *) self_;
+ lua_close (self->L);
+}
+
+struct plugin_vtable lua_plugin_vtable =
+{
+ .gc = lua_plugin_gc,
+ .free = lua_plugin_free,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The registry can be used as a cache for weakly referenced objects
+
+static bool
+lua_cache_get (lua_State *L, void *object)
+{
+ lua_rawgetp (L, LUA_REGISTRYINDEX, object);
+ if (lua_isnil (L, -1))
+ {
+ lua_pop (L, 1);
+ return false;
+ }
+ return true;
+}
+
+static void
+lua_cache_store (lua_State *L, void *object, int index)
+{
+ lua_pushvalue (L, index);
+ lua_rawsetp (L, LUA_REGISTRYINDEX, object);
+}
+
+static void
+lua_cache_invalidate (lua_State *L, void *object)
+{
+ lua_pushnil (L);
+ lua_rawsetp (L, LUA_REGISTRYINDEX, object);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Append a traceback to all errors so that we can later extract it
+static int
+lua_plugin_error_handler (lua_State *L)
+{
+ luaL_traceback (L, L, luaL_checkstring (L, 1), 1);
+ return 1;
+}
+
+static bool
+lua_plugin_process_error (struct lua_plugin *self, const char *message,
+ struct error **e)
+{
+ struct strv v = strv_make ();
+ cstr_split (message, "\n", true, &v);
+
+ if (v.len < 2)
+ error_set (e, "%s", message);
+ else
+ {
+ error_set (e, "%s", v.vector[0]);
+ log_global_debug (self->ctx, "Lua: plugin \"#s\": #s",
+ self->super.name, v.vector[1]);
+ for (size_t i = 2; i < v.len; i++)
+ log_global_debug (self->ctx, " #s", v.vector[i]);
+ }
+
+ strv_free (&v);
+ return false;
+}
+
+/// Call a Lua function and process errors using our special error handler
+static bool
+lua_plugin_call (struct lua_plugin *self,
+ int n_params, int n_results, struct error **e)
+{
+ // XXX: this may eventually be called from a thread, then this is wrong
+ lua_State *L = self->L;
+
+ // We need to pop the error handler at the end
+ lua_pushcfunction (L, lua_plugin_error_handler);
+ int error_handler_idx = -n_params - 2;
+ lua_insert (L, error_handler_idx);
+
+ if (!lua_pcall (L, n_params, n_results, error_handler_idx))
+ {
+ lua_remove (L, -n_results - 1);
+ return true;
+ }
+
+ (void) lua_plugin_process_error (self, lua_tostring (L, -1), e);
+ lua_pop (L, 2);
+ return false;
+}
+
+/// Convenience function; replaces the "original" string or produces an error
+static bool
+lua_plugin_handle_string_filter_result (struct lua_plugin *self,
+ char **original, bool utf8, struct error **e)
+{
+ lua_State *L = self->L;
+ if (lua_isnil (L, -1))
+ {
+ cstr_set (original, NULL);
+ return true;
+ }
+ if (!lua_isstring (L, -1))
+ return error_set (e, "must return either a string or nil");
+
+ size_t len;
+ const char *processed = lua_tolstring (L, -1, &len);
+ if (utf8 && !utf8_validate (processed, len))
+ return error_set (e, "must return valid UTF-8");
+
+ // Only replace the string if it's different
+ if (strcmp (processed, *original))
+ cstr_set (original, xstrdup (processed));
+ return true;
+}
+
+static const char *
+lua_plugin_check_utf8 (lua_State *L, int arg)
+{
+ size_t len;
+ const char *s = luaL_checklstring (L, arg, &len);
+ luaL_argcheck (L, utf8_validate (s, len), arg, "must be valid UTF-8");
+ return s;
+}
+
+static void
+lua_plugin_log_error
+ (struct lua_plugin *self, const char *where, struct error *error)
+{
+ log_global_error (self->ctx, "Lua: plugin \"#s\": #s: #s",
+ self->super.name, where, error->message);
+ error_free (error);
+}
+
+/// Pop "n" values from the stack into a table, using their indexes as keys
+static void
+lua_plugin_pack (lua_State *L, int n)
+{
+ lua_createtable (L, n, 0);
+ lua_insert (L, -n - 1);
+ for (int i = n; i; i--)
+ lua_rawseti (L, -i - 1, i);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+lua_plugin_kv (lua_State *L, const char *key, const char *value)
+{
+ lua_pushstring (L, value);
+ lua_setfield (L, -2, key);
+}
+
+static void
+lua_plugin_push_message (lua_State *L, const struct irc_message *msg)
+{
+ lua_createtable (L, 0, 4);
+
+ lua_createtable (L, msg->tags.len, 0);
+ struct str_map_iter iter = str_map_iter_make (&msg->tags);
+ const char *value;
+ while ((value = str_map_iter_next (&iter)))
+ lua_plugin_kv (L, iter.link->key, value);
+ lua_setfield (L, -2, "tags");
+
+ // TODO: parse the prefix further?
+ if (msg->prefix) lua_plugin_kv (L, "prefix", msg->prefix);
+ if (msg->command) lua_plugin_kv (L, "command", msg->command);
+
+ lua_createtable (L, msg->params.len, 0);
+ for (size_t i = 0; i < msg->params.len; i++)
+ {
+ lua_pushstring (L, msg->params.vector[i]);
+ lua_rawseti (L, -2, i + 1);
+ }
+ lua_setfield (L, -2, "params");
+}
+
+static int
+lua_plugin_parse (lua_State *L)
+{
+ struct irc_message msg;
+ irc_parse_message (&msg, luaL_checkstring (L, 1));
+ lua_plugin_push_message (L, &msg);
+ irc_free_message (&msg);
+ return 1;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Lua code can use weakly referenced wrappers for internal objects.
+
+typedef struct weak_ref_link *
+ (*lua_weak_ref_fn) (void *object, destroy_cb_fn cb, void *user_data);
+typedef void (*lua_weak_unref_fn) (void *object, struct weak_ref_link **link);
+
+struct lua_weak_info
+{
+ const char *name; ///< Metatable name
+ struct ispect_field *ispect; ///< Introspection data
+
+ lua_weak_ref_fn ref; ///< Weak link invalidator
+ lua_weak_unref_fn unref; ///< Weak link generator
+};
+
+struct lua_weak
+{
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ struct lua_weak_info *info; ///< Introspection data
+ void *object; ///< The object
+ struct weak_ref_link *weak_ref; ///< A weak reference link
+};
+
+static void
+lua_weak_invalidate (void *object, void *user_data)
+{
+ struct lua_weak *wrapper = user_data;
+ wrapper->object = NULL;
+ wrapper->weak_ref = NULL;
+ // This can in theory call the GC, order isn't arbitrary here
+ lua_cache_invalidate (wrapper->plugin->L, object);
+}
+
+static void
+lua_weak_push (lua_State *L, struct lua_plugin *plugin, void *object,
+ struct lua_weak_info *info)
+{
+ if (!object)
+ {
+ lua_pushnil (L);
+ return;
+ }
+ if (lua_cache_get (L, object))
+ return;
+
+ struct lua_weak *wrapper = lua_newuserdata (L, sizeof *wrapper);
+ luaL_setmetatable (L, info->name);
+ wrapper->plugin = plugin;
+ wrapper->info = info;
+ wrapper->object = object;
+ wrapper->weak_ref = NULL;
+ if (info->ref)
+ wrapper->weak_ref = info->ref (object, lua_weak_invalidate, wrapper);
+ lua_cache_store (L, object, -1);
+}
+
+static int
+lua_weak_gc (lua_State *L, const struct lua_weak_info *info)
+{
+ struct lua_weak *wrapper = luaL_checkudata (L, 1, info->name);
+ if (wrapper->object)
+ {
+ lua_cache_invalidate (L, wrapper->object);
+ if (info->unref)
+ info->unref (wrapper->object, &wrapper->weak_ref);
+ wrapper->object = NULL;
+ }
+ return 0;
+}
+
+static struct lua_weak *
+lua_weak_deref (lua_State *L, const struct lua_weak_info *info)
+{
+ struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
+ luaL_argcheck (L, weak->object, 1, "dead reference used");
+ return weak;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define LUA_WEAK_DECLARE(id, metatable_id) \
+ static struct lua_weak_info lua_ ## id ## _info = \
+ { \
+ .name = metatable_id, \
+ .ispect = g_ ## id ## _ispect, \
+ .ref = (lua_weak_ref_fn) id ## _weak_ref, \
+ .unref = (lua_weak_unref_fn) id ## _weak_unref, \
+ };
+
+#define XLUA_USER_METATABLE "user" ///< Identifier for Lua metatable
+#define XLUA_CHANNEL_METATABLE "channel" ///< Identifier for Lua metatable
+#define XLUA_BUFFER_METATABLE "buffer" ///< Identifier for Lua metatable
+#define XLUA_SERVER_METATABLE "server" ///< Identifier for Lua metatable
+
+LUA_WEAK_DECLARE (user, XLUA_USER_METATABLE)
+LUA_WEAK_DECLARE (channel, XLUA_CHANNEL_METATABLE)
+LUA_WEAK_DECLARE (buffer, XLUA_BUFFER_METATABLE)
+LUA_WEAK_DECLARE (server, XLUA_SERVER_METATABLE)
+
+// The global context is kind of fake and doesn't have any ref-counting,
+// however it's still very much an object
+static struct lua_weak_info lua_ctx_info =
+{
+ .name = PROGRAM_NAME,
+ .ispect = g_ctx_ispect,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_user_gc (lua_State *L)
+{
+ return lua_weak_gc (L, &lua_user_info);
+}
+
+static int
+lua_user_get_channels (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_user_info);
+ struct user *user = wrapper->object;
+
+ int i = 1;
+ lua_newtable (L);
+ LIST_FOR_EACH (struct user_channel, iter, user->channels)
+ {
+ lua_weak_push (L, wrapper->plugin, iter->channel, &lua_channel_info);
+ lua_rawseti (L, -2, i++);
+ }
+ return 1;
+}
+
+static luaL_Reg lua_user_table[] =
+{
+ { "__gc", lua_user_gc },
+ { "get_channels", lua_user_get_channels },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_channel_gc (lua_State *L)
+{
+ return lua_weak_gc (L, &lua_channel_info);
+}
+
+static int
+lua_channel_get_users (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_channel_info);
+ struct channel *channel = wrapper->object;
+
+ int i = 1;
+ lua_newtable (L);
+ LIST_FOR_EACH (struct channel_user, iter, channel->users)
+ {
+ lua_createtable (L, 0, 2);
+ lua_weak_push (L, wrapper->plugin, iter->user, &lua_user_info);
+ lua_setfield (L, -2, "user");
+ lua_plugin_kv (L, "prefixes", iter->prefixes);
+
+ lua_rawseti (L, -2, i++);
+ }
+ return 1;
+}
+
+static luaL_Reg lua_channel_table[] =
+{
+ { "__gc", lua_channel_gc },
+ { "get_users", lua_channel_get_users },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_buffer_gc (lua_State *L)
+{
+ return lua_weak_gc (L, &lua_buffer_info);
+}
+
+static int
+lua_buffer_log (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
+ struct buffer *buffer = wrapper->object;
+ const char *message = lua_plugin_check_utf8 (L, 2);
+ log_full (wrapper->plugin->ctx, buffer->server, buffer,
+ 0, BUFFER_LINE_STATUS, "#s", message);
+ return 0;
+}
+
+static int
+lua_buffer_execute (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
+ struct buffer *buffer = wrapper->object;
+ const char *line = lua_plugin_check_utf8 (L, 2);
+ (void) process_input_line (wrapper->plugin->ctx, buffer, line, 0);
+ return 0;
+}
+
+static luaL_Reg lua_buffer_table[] =
+{
+ { "__gc", lua_buffer_gc },
+ { "log", lua_buffer_log },
+ { "execute", lua_buffer_execute },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_server_gc (lua_State *L)
+{
+ return lua_weak_gc (L, &lua_server_info);
+}
+
+static const char *
+lua_server_state_to_string (enum server_state state)
+{
+ switch (state)
+ {
+ case IRC_DISCONNECTED: return "disconnected";
+ case IRC_CONNECTING: return "connecting";
+ case IRC_CONNECTED: return "connected";
+ case IRC_REGISTERED: return "registered";
+ case IRC_CLOSING: return "closing";
+ case IRC_HALF_CLOSED: return "half-closed";
+ }
+ return "?";
+}
+
+static int
+lua_server_get_state (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
+ struct server *server = wrapper->object;
+ lua_pushstring (L, lua_server_state_to_string (server->state));
+ return 1;
+}
+
+static int
+lua_server_send (lua_State *L)
+{
+ struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
+ struct server *server = wrapper->object;
+ irc_send (server, "%s", luaL_checkstring (L, 2));
+ return 0;
+}
+
+static luaL_Reg lua_server_table[] =
+{
+ { "__gc", lua_server_gc },
+ { "get_state", lua_server_get_state },
+ { "send", lua_server_send },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define XLUA_HOOK_METATABLE "hook" ///< Identifier for the Lua metatable
+
+enum lua_hook_type
+{
+ XLUA_HOOK_DEFUNCT, ///< No longer functional
+ XLUA_HOOK_INPUT, ///< Input hook
+ XLUA_HOOK_IRC, ///< IRC hook
+ XLUA_HOOK_PROMPT, ///< Prompt hook
+ XLUA_HOOK_COMPLETION, ///< Autocomplete
+};
+
+struct lua_hook
+{
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ enum lua_hook_type type; ///< Type of the hook
+ int ref_callback; ///< Reference to the callback
+ union
+ {
+ struct hook hook; ///< Hook base structure
+ struct input_hook input_hook; ///< Input hook
+ struct irc_hook irc_hook; ///< IRC hook
+ struct prompt_hook prompt_hook; ///< IRC hook
+ struct completion_hook c_hook; ///< Autocomplete hook
+ }
+ data; ///< Hook data
+};
+
+static int
+lua_hook_unhook (lua_State *L)
+{
+ struct lua_hook *hook = luaL_checkudata (L, 1, XLUA_HOOK_METATABLE);
+ switch (hook->type)
+ {
+ case XLUA_HOOK_INPUT:
+ LIST_UNLINK (hook->plugin->ctx->input_hooks, &hook->data.hook);
+ break;
+ case XLUA_HOOK_IRC:
+ LIST_UNLINK (hook->plugin->ctx->irc_hooks, &hook->data.hook);
+ break;
+ case XLUA_HOOK_PROMPT:
+ LIST_UNLINK (hook->plugin->ctx->prompt_hooks, &hook->data.hook);
+ refresh_prompt (hook->plugin->ctx);
+ break;
+ case XLUA_HOOK_COMPLETION:
+ LIST_UNLINK (hook->plugin->ctx->completion_hooks, &hook->data.hook);
+ break;
+ default:
+ hard_assert (!"invalid hook type");
+ case XLUA_HOOK_DEFUNCT:
+ break;
+ }
+
+ luaL_unref (L, LUA_REGISTRYINDEX, hook->ref_callback);
+ hook->ref_callback = LUA_REFNIL;
+
+ // The hook no longer has to stay alive
+ hook->type = XLUA_HOOK_DEFUNCT;
+ lua_cache_invalidate (L, hook);
+ return 0;
+}
+
+// The hook dies either when the plugin requests it or at plugin unload
+static luaL_Reg lua_hook_table[] =
+{
+ { "unhook", lua_hook_unhook },
+ { "__gc", lua_hook_unhook },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static char *
+lua_input_hook_filter (struct input_hook *self, struct buffer *buffer,
+ char *input)
+{
+ struct lua_hook *hook =
+ CONTAINER_OF (self, struct lua_hook, data.input_hook);
+ struct lua_plugin *plugin = hook->plugin;
+ lua_State *L = plugin->L;
+
+ lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
+ lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
+ lua_weak_push (L, plugin, buffer, &lua_buffer_info); // 2: buffer
+ lua_pushstring (L, input); // 3: input
+
+ struct error *e = NULL;
+ if (lua_plugin_call (plugin, 3, 1, &e))
+ {
+ lua_plugin_handle_string_filter_result (plugin, &input, true, &e);
+ lua_pop (L, 1);
+ }
+ if (e)
+ lua_plugin_log_error (plugin, "input hook", e);
+ return input;
+}
+
+static char *
+lua_irc_hook_filter (struct irc_hook *self, struct server *s, char *message)
+{
+ struct lua_hook *hook =
+ CONTAINER_OF (self, struct lua_hook, data.irc_hook);
+ struct lua_plugin *plugin = hook->plugin;
+ lua_State *L = plugin->L;
+
+ lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
+ lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
+ lua_weak_push (L, plugin, s, &lua_server_info); // 2: server
+ lua_pushstring (L, message); // 3: message
+
+ struct error *e = NULL;
+ if (lua_plugin_call (plugin, 3, 1, &e))
+ {
+ lua_plugin_handle_string_filter_result (plugin, &message, false, &e);
+ lua_pop (L, 1);
+ }
+ if (e)
+ lua_plugin_log_error (plugin, "IRC hook", e);
+ return message;
+}
+
+static char *
+lua_prompt_hook_make (struct prompt_hook *self)
+{
+ struct lua_hook *hook =
+ CONTAINER_OF (self, struct lua_hook, data.prompt_hook);
+ struct lua_plugin *plugin = hook->plugin;
+ lua_State *L = plugin->L;
+
+ lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
+ lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
+
+ struct error *e = NULL;
+ char *prompt = xstrdup ("");
+ if (lua_plugin_call (plugin, 1, 1, &e))
+ {
+ lua_plugin_handle_string_filter_result (plugin, &prompt, true, &e);
+ lua_pop (L, 1);
+ }
+ if (e)
+ lua_plugin_log_error (plugin, "prompt hook", e);
+ return prompt;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+lua_plugin_push_completion (lua_State *L, struct completion *data)
+{
+ lua_createtable (L, 0, 3);
+
+ lua_pushstring (L, data->line);
+ lua_setfield (L, -2, "line");
+
+ lua_createtable (L, data->words_len, 0);
+ for (size_t i = 0; i < data->words_len; i++)
+ {
+ lua_pushlstring (L, data->line + data->words[i].start,
+ data->words[i].end - data->words[i].start);
+ lua_rawseti (L, -2, i + 1);
+ }
+ lua_setfield (L, -2, "words");
+
+ lua_pushinteger (L, data->location);
+ lua_setfield (L, -2, "location");
+}
+
+static bool
+lua_completion_hook_process_value (lua_State *L, struct strv *output,
+ struct error **e)
+{
+ if (lua_type (L, -1) != LUA_TSTRING)
+ {
+ return error_set (e,
+ "%s: %s", "invalid type", lua_typename (L, lua_type (L, -1)));
+ }
+
+ size_t len;
+ const char *value = lua_tolstring (L, -1, &len);
+ if (!utf8_validate (value, len))
+ return error_set (e, "must be valid UTF-8");
+
+ strv_append (output, value);
+ return true;
+}
+
+static bool
+lua_completion_hook_process (lua_State *L, struct strv *output,
+ struct error **e)
+{
+ if (lua_isnil (L, -1))
+ return true;
+ if (!lua_istable (L, -1))
+ return error_set (e, "must return either a table or nil");
+
+ bool success = true;
+ for (lua_Integer i = 1; success && lua_rawgeti (L, -1, i); i++)
+ if ((success = lua_completion_hook_process_value (L, output, e)))
+ lua_pop (L, 1);
+ lua_pop (L, 1);
+ return success;
+}
+
+static void
+lua_completion_hook_complete (struct completion_hook *self,
+ struct completion *data, const char *word, struct strv *output)
+{
+ struct lua_hook *hook =
+ CONTAINER_OF (self, struct lua_hook, data.c_hook);
+ struct lua_plugin *plugin = hook->plugin;
+ lua_State *L = plugin->L;
+
+ lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
+ lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
+ lua_plugin_push_completion (L, data); // 2: data
+
+ lua_weak_push (L, plugin, plugin->ctx->current_buffer, &lua_buffer_info);
+ lua_setfield (L, -2, "buffer");
+
+ lua_pushstring (L, word); // 3: word
+
+ struct error *e = NULL;
+ if (lua_plugin_call (plugin, 3, 1, &e))
+ {
+ lua_completion_hook_process (L, output, &e);
+ lua_pop (L, 1);
+ }
+ if (e)
+ lua_plugin_log_error (plugin, "autocomplete hook", e);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct lua_hook *
+lua_plugin_push_hook (lua_State *L, struct lua_plugin *plugin,
+ int callback_index, enum lua_hook_type type, int priority)
+{
+ luaL_checktype (L, callback_index, LUA_TFUNCTION);
+
+ struct lua_hook *hook = lua_newuserdata (L, sizeof *hook);
+ luaL_setmetatable (L, XLUA_HOOK_METATABLE);
+ memset (hook, 0, sizeof *hook);
+ hook->data.hook.priority = priority;
+ hook->type = type;
+ hook->plugin = plugin;
+
+ lua_pushvalue (L, callback_index);
+ hook->ref_callback = luaL_ref (L, LUA_REGISTRYINDEX);
+
+ // Make sure the hook doesn't get garbage collected and return it
+ lua_cache_store (L, hook, -1);
+ return hook;
+}
+
+static int
+lua_plugin_hook_input (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ struct lua_hook *hook = lua_plugin_push_hook
+ (L, plugin, 1, XLUA_HOOK_INPUT, luaL_optinteger (L, 2, 0));
+ hook->data.input_hook.filter = lua_input_hook_filter;
+ plugin->ctx->input_hooks =
+ hook_insert (plugin->ctx->input_hooks, &hook->data.hook);
+ return 1;
+}
+
+static int
+lua_plugin_hook_irc (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ struct lua_hook *hook = lua_plugin_push_hook
+ (L, plugin, 1, XLUA_HOOK_IRC, luaL_optinteger (L, 2, 0));
+ hook->data.irc_hook.filter = lua_irc_hook_filter;
+ plugin->ctx->irc_hooks =
+ hook_insert (plugin->ctx->irc_hooks, &hook->data.hook);
+ return 1;
+}
+
+static int
+lua_plugin_hook_prompt (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ struct lua_hook *hook = lua_plugin_push_hook
+ (L, plugin, 1, XLUA_HOOK_PROMPT, luaL_optinteger (L, 2, 0));
+ hook->data.prompt_hook.make = lua_prompt_hook_make;
+ plugin->ctx->prompt_hooks =
+ hook_insert (plugin->ctx->prompt_hooks, &hook->data.hook);
+ refresh_prompt (plugin->ctx);
+ return 1;
+}
+
+static int
+lua_plugin_hook_completion (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ struct lua_hook *hook = lua_plugin_push_hook
+ (L, plugin, 1, XLUA_HOOK_COMPLETION, luaL_optinteger (L, 2, 0));
+ hook->data.c_hook.complete = lua_completion_hook_complete;
+ plugin->ctx->completion_hooks =
+ hook_insert (plugin->ctx->completion_hooks, &hook->data.hook);
+ return 1;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define XLUA_SCHEMA_METATABLE "schema" ///< Identifier for the Lua metatable
+
+struct lua_schema_item
+{
+ LIST_HEADER (struct lua_schema_item)
+
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ struct config_item *item; ///< The item managed by the schema
+ struct config_schema schema; ///< Schema itself
+
+ int ref_validate; ///< Reference to "validate" callback
+ int ref_on_change; ///< Reference to "on_change" callback
+};
+
+static void
+lua_schema_item_discard (struct lua_schema_item *self)
+{
+ if (self->item)
+ {
+ self->item->schema = NULL;
+ self->item->user_data = NULL;
+ self->item = NULL;
+ LIST_UNLINK (self->plugin->schemas, self);
+ }
+
+ // Now that we've disconnected from the item, allow garbage collection
+ lua_cache_invalidate (self->plugin->L, self);
+}
+
+static int
+lua_schema_item_gc (lua_State *L)
+{
+ struct lua_schema_item *self =
+ luaL_checkudata (L, 1, XLUA_SCHEMA_METATABLE);
+ lua_schema_item_discard (self);
+
+ free ((char *) self->schema.name);
+ free ((char *) self->schema.comment);
+ free ((char *) self->schema.default_);
+
+ luaL_unref (L, LUA_REGISTRYINDEX, self->ref_validate);
+ luaL_unref (L, LUA_REGISTRYINDEX, self->ref_on_change);
+ return 0;
+}
+
+static luaL_Reg lua_schema_table[] =
+{
+ { "__gc", lua_schema_item_gc },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Unfortunately this has the same problem as JSON libraries in that Lua
+/// cannot store null values in containers (it has no distinct "undefined" type)
+static void
+lua_plugin_push_config_item (lua_State *L, const struct config_item *item)
+{
+ switch (item->type)
+ {
+ case CONFIG_ITEM_NULL:
+ lua_pushnil (L);
+ break;
+ case CONFIG_ITEM_OBJECT:
+ {
+ lua_createtable (L, 0, item->value.object.len);
+
+ struct str_map_iter iter = str_map_iter_make (&item->value.object);
+ struct config_item *child;
+ while ((child = str_map_iter_next (&iter)))
+ {
+ lua_plugin_push_config_item (L, child);
+ lua_setfield (L, -2, iter.link->key);
+ }
+ break;
+ }
+ case CONFIG_ITEM_BOOLEAN:
+ lua_pushboolean (L, item->value.boolean);
+ break;
+ case CONFIG_ITEM_INTEGER:
+ lua_pushinteger (L, item->value.integer);
+ break;
+ case CONFIG_ITEM_STRING:
+ case CONFIG_ITEM_STRING_ARRAY:
+ lua_pushlstring (L, item->value.string.str, item->value.string.len);
+ break;
+ }
+}
+
+static bool
+lua_schema_item_validate (const struct config_item *item, struct error **e)
+{
+ struct lua_schema_item *self = item->user_data;
+ if (self->ref_validate == LUA_REFNIL)
+ return true;
+
+ lua_State *L = self->plugin->L;
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_validate);
+ lua_plugin_push_config_item (L, item);
+
+ // The callback can make use of error("...", 0) to produce nice messages
+ return lua_plugin_call (self->plugin, 1, 0, e);
+}
+
+static void
+lua_schema_item_on_change (struct config_item *item)
+{
+ struct lua_schema_item *self = item->user_data;
+ if (self->ref_on_change == LUA_REFNIL)
+ return;
+
+ lua_State *L = self->plugin->L;
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_change);
+ lua_plugin_push_config_item (L, item);
+
+ struct error *e = NULL;
+ if (!lua_plugin_call (self->plugin, 1, 0, &e))
+ lua_plugin_log_error (self->plugin, "schema on_change", e);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_plugin_decode_config_item_type (const char *type)
+{
+ if (!strcmp (type, "null")) return CONFIG_ITEM_NULL;
+ if (!strcmp (type, "object")) return CONFIG_ITEM_OBJECT;
+ if (!strcmp (type, "boolean")) return CONFIG_ITEM_BOOLEAN;
+ if (!strcmp (type, "integer")) return CONFIG_ITEM_INTEGER;
+ if (!strcmp (type, "string")) return CONFIG_ITEM_STRING;
+ if (!strcmp (type, "string_array")) return CONFIG_ITEM_STRING_ARRAY;
+ return -1;
+}
+
+static bool
+lua_plugin_check_field (lua_State *L, int idx, const char *name,
+ int expected, bool optional)
+{
+ int found = lua_getfield (L, idx, name);
+ if (found == expected)
+ return true;
+ if (optional && found == LUA_TNIL)
+ return false;
+
+ const char *message = optional
+ ? "invalid field \"%s\" (found: %s, expected: %s or nil)"
+ : "invalid or missing field \"%s\" (found: %s, expected: %s)";
+ return luaL_error (L, message, name,
+ lua_typename (L, found), lua_typename (L, expected));
+}
+
+static int
+lua_plugin_add_config_schema (lua_State *L, struct lua_plugin *plugin,
+ struct config_item *subtree, const char *name)
+{
+ struct config_item *item = str_map_find (&subtree->value.object, name);
+ // This should only ever happen because of a conflict with another plugin;
+ // this is the price we pay for simplicity
+ if (item && item->schema)
+ {
+ struct lua_schema_item *owner = item->user_data;
+ return luaL_error (L, "conflicting schema item: %s (owned by: %s)",
+ name, owner->plugin->super.name);
+ }
+
+ // Create and initialize a full userdata wrapper for the schema item
+ struct lua_schema_item *self = lua_newuserdata (L, sizeof *self);
+ luaL_setmetatable (L, XLUA_SCHEMA_METATABLE);
+ memset (self, 0, sizeof *self);
+
+ self->plugin = plugin;
+ self->ref_on_change = LUA_REFNIL;
+ self->ref_validate = LUA_REFNIL;
+
+ struct config_schema *schema = &self->schema;
+ schema->name = xstrdup (name);
+ schema->comment = NULL;
+ schema->default_ = NULL;
+ schema->type = CONFIG_ITEM_NULL;
+ schema->on_change = lua_schema_item_on_change;
+ schema->validate = lua_schema_item_validate;
+
+ // Try to update the defaults with values provided by the plugin
+ int values = lua_absindex (L, -2);
+ (void) lua_plugin_check_field (L, values, "type", LUA_TSTRING, false);
+ int item_type = schema->type =
+ lua_plugin_decode_config_item_type (lua_tostring (L, -1));
+ if (item_type == -1)
+ return luaL_error (L, "invalid type of schema item");
+
+ if (lua_plugin_check_field (L, values, "comment", LUA_TSTRING, true))
+ schema->comment = xstrdup (lua_tostring (L, -1));
+ if (lua_plugin_check_field (L, values, "default", LUA_TSTRING, true))
+ schema->default_ = xstrdup (lua_tostring (L, -1));
+
+ lua_pop (L, 3);
+
+ (void) lua_plugin_check_field (L, values, "on_change", LUA_TFUNCTION, true);
+ self->ref_on_change = luaL_ref (L, LUA_REGISTRYINDEX);
+ (void) lua_plugin_check_field (L, values, "validate", LUA_TFUNCTION, true);
+ self->ref_validate = luaL_ref (L, LUA_REGISTRYINDEX);
+
+ // Try to install the created schema item into our configuration
+ struct error *warning = NULL, *e = NULL;
+ item = config_schema_initialize_item
+ (&self->schema, subtree, self, &warning, &e);
+
+ if (warning)
+ {
+ log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s",
+ plugin->super.name, warning->message);
+ error_free (warning);
+ }
+ if (e)
+ {
+ const char *error = lua_pushstring (L, e->message);
+ error_free (e);
+ return luaL_error (L, "%s", error);
+ }
+
+ self->item = item;
+ LIST_PREPEND (plugin->schemas, self);
+
+ // On the stack there should be the schema table and the resulting object;
+ // we need to make sure Lua doesn't GC the second and get rid of them both
+ lua_cache_store (L, self, -1);
+ lua_pop (L, 2);
+ return 0;
+}
+
+static int
+lua_plugin_setup_config (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ luaL_checktype (L, 1, LUA_TTABLE);
+
+ struct app_context *ctx = plugin->ctx;
+ char *config_name = plugin_config_name (&plugin->super);
+ if (!config_name)
+ return luaL_error (L, "unsuitable plugin name");
+
+ struct str_map *plugins = get_plugins_config (ctx);
+ struct config_item *subtree = str_map_find (plugins, config_name);
+ if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
+ str_map_set (plugins, config_name, (subtree = config_item_object ()));
+ free (config_name);
+
+ LIST_FOR_EACH (struct lua_schema_item, iter, plugin->schemas)
+ lua_schema_item_discard (iter);
+
+ // Load all schema items and apply them to the plugin's subtree
+ lua_pushnil (L);
+ while (lua_next (L, 1))
+ {
+ if (lua_type (L, -2) != LUA_TSTRING
+ || lua_type (L, -1) != LUA_TTABLE)
+ return luaL_error (L, "%s: %s -> %s", "invalid types",
+ lua_typename (L, lua_type (L, -2)),
+ lua_typename (L, lua_type (L, -1)));
+ lua_plugin_add_config_schema (L, plugin, subtree, lua_tostring (L, -2));
+ }
+
+ // Let the plugin read out configuration via on_change callbacks
+ config_schema_call_changed (subtree);
+ return 0;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Identifier for the Lua metatable
+#define XLUA_CONNECTION_METATABLE "connection"
+
+struct lua_connection
+{
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ struct poller_fd socket_event; ///< Socket is ready
+ int socket_fd; ///< Underlying connected socket
+
+ bool got_eof; ///< Half-closed by remote host
+ bool closing; ///< We're closing the connection
+
+ struct str read_buffer; ///< Read buffer
+ struct str write_buffer; ///< Write buffer
+};
+
+static void
+lua_connection_update_poller (struct lua_connection *self)
+{
+ poller_fd_set (&self->socket_event,
+ self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
+}
+
+static int
+lua_connection_send (lua_State *L)
+{
+ struct lua_connection *self =
+ luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
+ if (self->socket_fd == -1)
+ return luaL_error (L, "connection has been closed");
+
+ size_t len;
+ const char *s = luaL_checklstring (L, 2, &len);
+ str_append_data (&self->write_buffer, s, len);
+ lua_connection_update_poller (self);
+ return 0;
+}
+
+static void
+lua_connection_discard (struct lua_connection *self)
+{
+ if (self->socket_fd != -1)
+ {
+ poller_fd_reset (&self->socket_event);
+ xclose (self->socket_fd);
+ self->socket_fd = -1;
+
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+ }
+
+ // Connection is dead, we don't need to hold onto any resources anymore
+ lua_cache_invalidate (self->plugin->L, self);
+}
+
+static int
+lua_connection_close (lua_State *L)
+{
+ struct lua_connection *self =
+ luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
+ if (self->socket_fd != -1)
+ {
+ self->closing = true;
+ // NOTE: this seems to do nothing on Linux
+ (void) shutdown (self->socket_fd, SHUT_RD);
+
+ // Right now we want to wait until all data is flushed to the socket
+ // and can't call close() here immediately -- a rewrite to use async
+ // would enable the user to await on either :send() or :flush();
+ // a successful send() doesn't necessarily mean anything though
+ if (!self->write_buffer.len)
+ lua_connection_discard (self);
+ }
+ return 0;
+}
+
+static int
+lua_connection_gc (lua_State *L)
+{
+ lua_connection_discard (luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE));
+ return 0;
+}
+
+static luaL_Reg lua_connection_table[] =
+{
+ { "send", lua_connection_send },
+ { "close", lua_connection_close },
+ { "__gc", lua_connection_gc },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_connection_check_fn (lua_State *L)
+{
+ lua_plugin_check_field (L, 1, luaL_checkstring (L, 2), LUA_TFUNCTION, true);
+ return 1;
+}
+
+// We need to run it in a protected environment because of lua_getfield()
+static bool
+lua_connection_cb_lookup (struct lua_connection *self, const char *name,
+ struct error **e)
+{
+ lua_State *L = self->plugin->L;
+ lua_pushcfunction (L, lua_connection_check_fn);
+ hard_assert (lua_cache_get (L, self));
+ lua_pushstring (L, name);
+ return lua_plugin_call (self->plugin, 2, 1, e);
+}
+
+// Ideally lua_connection_cb_lookup() would return a ternary value
+static bool
+lua_connection_eat_nil (struct lua_connection *self)
+{
+ if (lua_toboolean (self->plugin->L, -1))
+ return false;
+ lua_pop (self->plugin->L, 1);
+ return true;
+}
+
+static bool
+lua_connection_invoke_on_data (struct lua_connection *self, struct error **e)
+{
+ if (!lua_connection_cb_lookup (self, "on_data", e))
+ return false;
+ if (lua_connection_eat_nil (self))
+ return true;
+
+ lua_pushlstring (self->plugin->L,
+ self->read_buffer.str, self->read_buffer.len);
+ return lua_plugin_call (self->plugin, 1, 0, e);
+}
+
+static bool
+lua_connection_invoke_on_eof (struct lua_connection *self, struct error **e)
+{
+ if (!lua_connection_cb_lookup (self, "on_eof", e))
+ return false;
+ if (lua_connection_eat_nil (self))
+ return true;
+ return lua_plugin_call (self->plugin, 0, 0, e);
+}
+
+static bool
+lua_connection_invoke_on_error (struct lua_connection *self,
+ const char *error, struct error **e)
+{
+ // XXX: not sure if ignoring errors after :close() is always desired;
+ // code might want to make sure that data are transferred successfully
+ if (!self->closing
+ && lua_connection_cb_lookup (self, "on_error", e)
+ && !lua_connection_eat_nil (self))
+ {
+ lua_pushstring (self->plugin->L, error);
+ lua_plugin_call (self->plugin, 1, 0, e);
+ }
+ return false;
+}
+
+static bool
+lua_connection_try_read (struct lua_connection *self, struct error **e)
+{
+ // Avoid the read call when it's obviously not going to return any data
+ // and would only cause unwanted invocation of callbacks
+ if (self->closing || self->got_eof)
+ return true;
+
+ enum socket_io_result read_result =
+ socket_io_try_read (self->socket_fd, &self->read_buffer);
+ const char *error = strerror (errno);
+
+ // Dispatch any data that we got before an EOF or any error
+ if (self->read_buffer.len)
+ {
+ if (!lua_connection_invoke_on_data (self, e))
+ return false;
+ str_reset (&self->read_buffer);
+ }
+
+ if (read_result == SOCKET_IO_EOF)
+ {
+ if (!lua_connection_invoke_on_eof (self, e))
+ return false;
+ self->got_eof = true;
+ }
+ if (read_result == SOCKET_IO_ERROR)
+ return lua_connection_invoke_on_error (self, error, e);
+ return true;
+}
+
+static bool
+lua_connection_try_write (struct lua_connection *self, struct error **e)
+{
+ enum socket_io_result write_result =
+ socket_io_try_write (self->socket_fd, &self->write_buffer);
+ const char *error = strerror (errno);
+
+ if (write_result == SOCKET_IO_ERROR)
+ return lua_connection_invoke_on_error (self, error, e);
+ return !self->closing || self->write_buffer.len;
+}
+
+static void
+lua_connection_on_ready (const struct pollfd *pfd, struct lua_connection *self)
+{
+ (void) pfd;
+
+ // Hold a reference so that it doesn't get collected on close()
+ hard_assert (lua_cache_get (self->plugin->L, self));
+
+ struct error *e = NULL;
+ bool keep = lua_connection_try_read (self, &e)
+ && lua_connection_try_write (self, &e);
+ if (e)
+ lua_plugin_log_error (self->plugin, "network I/O", e);
+ if (keep)
+ lua_connection_update_poller (self);
+ else
+ lua_connection_discard (self);
+
+ lua_pop (self->plugin->L, 1);
+}
+
+static struct lua_connection *
+lua_plugin_push_connection (struct lua_plugin *plugin, int socket_fd)
+{
+ lua_State *L = plugin->L;
+
+ struct lua_connection *self = lua_newuserdata (L, sizeof *self);
+ luaL_setmetatable (L, XLUA_CONNECTION_METATABLE);
+ memset (self, 0, sizeof *self);
+ self->plugin = plugin;
+
+ set_blocking (socket_fd, false);
+ self->socket_event = poller_fd_make
+ (&plugin->ctx->poller, (self->socket_fd = socket_fd));
+ self->socket_event.dispatcher = (poller_fd_fn) lua_connection_on_ready;
+ self->socket_event.user_data = self;
+ poller_fd_set (&self->socket_event, POLLIN);
+
+ self->read_buffer = str_make ();
+ self->write_buffer = str_make ();
+
+ // Make sure the connection doesn't get garbage collected and return it
+ lua_cache_store (L, self, -1);
+ return self;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The script can create as many wait channels as wanted. They only actually
+// do anything once they get yielded to the main lua_resume() call.
+
+/// Identifier for the Lua metatable
+#define XLUA_WCHANNEL_METATABLE "wchannel"
+
+struct lua_wait_channel
+{
+ LIST_HEADER (struct lua_wait_channel)
+
+ struct lua_task *task; ///< The task we're active in
+
+ /// Check if the event is ready and eventually push values to the thread;
+ /// the channel then may release any resources
+ bool (*check) (struct lua_wait_channel *self);
+
+ /// Release all resources held by the subclass
+ void (*cleanup) (struct lua_wait_channel *self);
+};
+
+static int
+lua_wchannel_gc (lua_State *L)
+{
+ struct lua_wait_channel *self =
+ luaL_checkudata (L, 1, XLUA_WCHANNEL_METATABLE);
+ if (self->cleanup)
+ self->cleanup (self);
+ return 0;
+}
+
+static luaL_Reg lua_wchannel_table[] =
+{
+ { "__gc", lua_wchannel_gc },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// A task encapsulates a thread so that wait channels yielded from its main
+// function get waited upon by the event loop
+
+#define XLUA_TASK_METATABLE "task" ///< Identifier for the Lua metatable
+
+struct lua_task
+{
+ LIST_HEADER (struct lua_task)
+
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ lua_State *thread; ///< Lua thread
+ struct lua_wait_channel *active; ///< Channels we're waiting on
+ struct poller_idle idle; ///< Idle job
+};
+
+static void
+lua_task_unregister_channels (struct lua_task *self)
+{
+ LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
+ {
+ iter->task = NULL;
+ LIST_UNLINK (self->active, iter);
+ lua_cache_invalidate (self->plugin->L, iter);
+ }
+}
+
+static void
+lua_task_cancel_internal (struct lua_task *self)
+{
+ if (self->thread)
+ {
+ lua_cache_invalidate (self->plugin->L, self->thread);
+ self->thread = NULL;
+ }
+ lua_task_unregister_channels (self);
+ poller_idle_reset (&self->idle);
+
+ // The task no longer has to stay alive
+ lua_cache_invalidate (self->plugin->L, self);
+}
+
+static int
+lua_task_cancel (lua_State *L)
+{
+ struct lua_task *self = luaL_checkudata (L, 1, XLUA_TASK_METATABLE);
+ // We could also yield and make lua_task_resume() check "self->thread",
+ // however the main issue here is that the script should just return
+ luaL_argcheck (L, L != self->thread, 1,
+ "cannot cancel task from within itself");
+ lua_task_cancel_internal (self);
+ return 0;
+}
+
+#define lua_task_wakeup(self) poller_idle_set (&(self)->idle)
+
+static bool
+lua_task_schedule (struct lua_task *self, int n, struct error **e)
+{
+ lua_State *L = self->thread;
+ for (int i = -1; -i <= n; i--)
+ {
+ struct lua_wait_channel *channel =
+ luaL_testudata (L, i, XLUA_WCHANNEL_METATABLE);
+ if (!channel)
+ return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
+ "tasks can only yield wait channels");
+ if (channel->task)
+ return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
+ "wait channels can only be active in one task at most");
+ }
+ for (int i = -1; -i <= n; i--)
+ {
+ // Quietly ignore duplicate channels
+ struct lua_wait_channel *channel = lua_touserdata (L, i);
+ if (channel->task)
+ continue;
+
+ // By going in reverse the list ends up in the right order
+ channel->task = self;
+ LIST_PREPEND (self->active, channel);
+ lua_cache_store (self->plugin->L, channel, i);
+ }
+ lua_pop (L, n);
+
+ // There doesn't have to be a single channel
+ // We can also be waiting on a channel that is already ready
+ lua_task_wakeup (self);
+ return true;
+}
+
+static void
+lua_task_resume (struct lua_task *self, int index)
+{
+ lua_State *L = self->thread;
+ bool waiting_on_multiple = self->active && self->active->next;
+
+ // Since we've ended the wait, we don't need to hold on to them anymore
+ lua_task_unregister_channels (self);
+
+ // On the first run we also have the main function on the stack,
+ // before any initial arguments
+ int n = lua_gettop (L) - (lua_status (L) == LUA_OK);
+
+ // Pack the values in a table and prepend the index of the channel, so that
+ // the caller doesn't need to care about the number of return values
+ if (waiting_on_multiple)
+ {
+ lua_plugin_pack (L, n);
+ lua_pushinteger (L, index);
+ lua_insert (L, -2);
+ n = 2;
+ }
+
+#if LUA_VERSION_NUM >= 504
+ int nresults = 0;
+ int res = lua_resume (L, NULL, n, &nresults);
+#else
+ int res = lua_resume (L, NULL, n);
+ int nresults = lua_gettop (L);
+#endif
+
+ struct error *error = NULL;
+ if (res == LUA_YIELD)
+ {
+ // AFAIK we don't get any good error context information from here
+ if (lua_task_schedule (self, nresults, &error))
+ return;
+ }
+ // For simplicity ignore any results from successful returns
+ else if (res != LUA_OK)
+ {
+ luaL_traceback (L, L, lua_tostring (L, -1), 0 /* or 1? */);
+ lua_plugin_process_error (self->plugin, lua_tostring (L, -1), &error);
+ lua_pop (L, 2);
+ }
+ if (error)
+ lua_plugin_log_error (self->plugin, "task", error);
+ lua_task_cancel_internal (self);
+}
+
+static void
+lua_task_check (struct lua_task *self)
+{
+ poller_idle_reset (&self->idle);
+
+ lua_Integer i = 0;
+ LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
+ {
+ i++;
+ if (iter->check (iter))
+ {
+ lua_task_resume (self, i);
+ return;
+ }
+ }
+ if (!self->active)
+ lua_task_resume (self, i);
+}
+
+// The task dies either when it finishes, it is cancelled, or at plugin unload
+static luaL_Reg lua_task_table[] =
+{
+ { "cancel", lua_task_cancel },
+ { "__gc", lua_task_cancel },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct lua_wait_timer
+{
+ struct lua_wait_channel super; ///< The structure we're deriving
+ struct poller_timer timer; ///< Timer event
+ bool expired; ///< Whether the timer has expired
+};
+
+static bool
+lua_wait_timer_check (struct lua_wait_channel *wchannel)
+{
+ struct lua_wait_timer *self =
+ CONTAINER_OF (wchannel, struct lua_wait_timer, super);
+ return self->super.task && self->expired;
+}
+
+static void
+lua_wait_timer_cleanup (struct lua_wait_channel *wchannel)
+{
+ struct lua_wait_timer *self =
+ CONTAINER_OF (wchannel, struct lua_wait_timer, super);
+ poller_timer_reset (&self->timer);
+}
+
+static void
+lua_wait_timer_dispatch (struct lua_wait_timer *self)
+{
+ self->expired = true;
+ if (self->super.task)
+ lua_task_wakeup (self->super.task);
+}
+
+static int
+lua_plugin_push_wait_timer (struct lua_plugin *plugin, lua_State *L,
+ lua_Integer timeout)
+{
+ struct lua_wait_timer *self = lua_newuserdata (L, sizeof *self);
+ luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
+ memset (self, 0, sizeof *self);
+
+ self->super.check = lua_wait_timer_check;
+ self->super.cleanup = lua_wait_timer_cleanup;
+
+ self->timer = poller_timer_make (&plugin->ctx->poller);
+ self->timer.dispatcher = (poller_timer_fn) lua_wait_timer_dispatch;
+ self->timer.user_data = self;
+
+ if (timeout)
+ poller_timer_set (&self->timer, timeout);
+ else
+ self->expired = true;
+ return 1;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct lua_wait_dial
+{
+ struct lua_wait_channel super; ///< The structure we're deriving
+
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ struct connector connector; ///< Connector object
+ bool active; ///< Whether the connector is alive
+
+ struct lua_connection *connection; ///< Established connection
+ char *hostname; ///< Target hostname
+ char *last_error; ///< Connecting error, if any
+};
+
+static bool
+lua_wait_dial_check (struct lua_wait_channel *wchannel)
+{
+ struct lua_wait_dial *self =
+ CONTAINER_OF (wchannel, struct lua_wait_dial, super);
+
+ lua_State *L = self->super.task->thread;
+ if (self->connection)
+ {
+ // FIXME: this way the connection may leak -- we pass the value to the
+ // task manager on the stack and forget about it but still leave the
+ // connection in the cache. That is because right now, when Lua code
+ // sets up callbacks in the connection object and returns, it might
+ // get otherwise GC'd since nothing else keeps referencing it.
+ // By rewriting lua_connection using async, tasks and wait channels
+ // would hold a reference, allowing us to remove it from the cache.
+ lua_cache_get (L, self->connection);
+ lua_pushstring (L, self->hostname);
+ self->connection = NULL;
+ }
+ else if (self->last_error)
+ {
+ lua_pushnil (L);
+ lua_pushnil (L);
+ lua_pushstring (L, self->last_error);
+ }
+ else
+ return false;
+ return true;
+}
+
+static void
+lua_wait_dial_cancel (struct lua_wait_dial *self)
+{
+ if (self->active)
+ {
+ connector_free (&self->connector);
+ self->active = false;
+ }
+}
+
+static void
+lua_wait_dial_cleanup (struct lua_wait_channel *wchannel)
+{
+ struct lua_wait_dial *self =
+ CONTAINER_OF (wchannel, struct lua_wait_dial, super);
+
+ lua_wait_dial_cancel (self);
+ if (self->connection)
+ lua_connection_discard (self->connection);
+
+ free (self->hostname);
+ free (self->last_error);
+}
+
+static void
+lua_wait_dial_on_connected (void *user_data, int socket, const char *hostname)
+{
+ struct lua_wait_dial *self = user_data;
+ if (self->super.task)
+ lua_task_wakeup (self->super.task);
+
+ self->connection = lua_plugin_push_connection (self->plugin, socket);
+ // TODO: use the hostname for SNI once TLS is implemented
+ self->hostname = xstrdup (hostname);
+ lua_wait_dial_cancel (self);
+}
+
+static void
+lua_wait_dial_on_failure (void *user_data)
+{
+ struct lua_wait_dial *self = user_data;
+ if (self->super.task)
+ lua_task_wakeup (self->super.task);
+ lua_wait_dial_cancel (self);
+}
+
+static void
+lua_wait_dial_on_error (void *user_data, const char *error)
+{
+ struct lua_wait_dial *self = user_data;
+ cstr_set (&self->last_error, xstrdup (error));
+}
+
+static int
+lua_plugin_push_wait_dial (struct lua_plugin *plugin, lua_State *L,
+ const char *host, const char *service)
+{
+ struct lua_wait_dial *self = lua_newuserdata (L, sizeof *self);
+ luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
+ memset (self, 0, sizeof *self);
+
+ self->super.check = lua_wait_dial_check;
+ self->super.cleanup = lua_wait_dial_cleanup;
+
+ struct connector *connector = &self->connector;
+ connector_init (connector, &plugin->ctx->poller);
+ connector_add_target (connector, host, service);
+
+ connector->on_connected = lua_wait_dial_on_connected;
+ connector->on_connecting = NULL;
+ connector->on_error = lua_wait_dial_on_error;
+ connector->on_failure = lua_wait_dial_on_failure;
+ connector->user_data = self;
+
+ self->plugin = plugin;
+ self->active = true;
+ return 1;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_async_go (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ luaL_checktype (L, 1, LUA_TFUNCTION);
+
+ lua_State *thread = lua_newthread (L);
+ lua_cache_store (L, thread, -1);
+ lua_pop (L, 1);
+
+ // Move the main function w/ arguments to the thread
+ lua_xmove (L, thread, lua_gettop (L));
+
+ struct lua_task *task = lua_newuserdata (L, sizeof *task);
+ luaL_setmetatable (L, XLUA_TASK_METATABLE);
+ memset (task, 0, sizeof *task);
+ task->plugin = plugin;
+ task->thread = thread;
+
+ task->idle = poller_idle_make (&plugin->ctx->poller);
+ task->idle.dispatcher = (poller_idle_fn) lua_task_check;
+ task->idle.user_data = task;
+ poller_idle_set (&task->idle);
+
+ // Make sure the task doesn't get garbage collected and return it
+ lua_cache_store (L, task, -1);
+ return 1;
+}
+
+static int
+lua_async_timer_ms (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ lua_Integer timeout = luaL_checkinteger (L, 1);
+ if (timeout < 0)
+ luaL_argerror (L, 1, "timeout mustn't be negative");
+ return lua_plugin_push_wait_timer (plugin, L, timeout);
+}
+
+static int
+lua_async_dial (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ return lua_plugin_push_wait_dial (plugin, L,
+ luaL_checkstring (L, 1), luaL_checkstring (L, 2));
+}
+
+static luaL_Reg lua_async_library[] =
+{
+ { "go", lua_async_go },
+ { "timer_ms", lua_async_timer_ms },
+ { "dial", lua_async_dial },
+ { NULL, NULL },
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_plugin_get_screen_size (lua_State *L)
+{
+ lua_pushinteger (L, g_terminal.lines);
+ lua_pushinteger (L, g_terminal.columns);
+ return 2;
+}
+
+static int
+lua_plugin_measure (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ const char *line = lua_plugin_check_utf8 (L, 1);
+
+ size_t term_len = 0, processed = 0, width = 0, len;
+ char *term = iconv_xstrdup (plugin->ctx->term_from_utf8,
+ (char *) line, strlen (line) + 1, &term_len);
+
+ mbstate_t ps;
+ memset (&ps, 0, sizeof ps);
+
+ wchar_t wch;
+ while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
+ {
+ hard_assert (len != (size_t) -2 && len != (size_t) -1);
+ hard_assert ((processed += len) <= term_len);
+
+ int wch_width = wcwidth (wch);
+ width += MAX (0, wch_width);
+ }
+ free (term);
+ lua_pushinteger (L, width);
+ return 1;
+}
+
+static int
+lua_ctx_gc (lua_State *L)
+{
+ return lua_weak_gc (L, &lua_ctx_info);
+}
+
+static luaL_Reg lua_plugin_library[] =
+{
+ // These are pseudo-global functions:
+
+ { "measure", lua_plugin_measure },
+ { "parse", lua_plugin_parse },
+ { "hook_input", lua_plugin_hook_input },
+ { "hook_irc", lua_plugin_hook_irc },
+ { "hook_prompt", lua_plugin_hook_prompt },
+ { "hook_completion", lua_plugin_hook_completion },
+ { "setup_config", lua_plugin_setup_config },
+
+ // And these are methods:
+
+ // Note that this only returns the height when used through an accessor.
+ { "get_screen_size", lua_plugin_get_screen_size },
+ { "__gc", lua_ctx_gc },
+ { NULL, NULL },
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void *
+lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
+{
+ (void) ud;
+ (void) o_size;
+
+ if (n_size)
+ return realloc (ptr, n_size);
+
+ free (ptr);
+ return NULL;
+}
+
+static int
+lua_plugin_panic (lua_State *L)
+{
+ // XXX: we might be able to do something better
+ print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
+ lua_close (L);
+ exit (EXIT_FAILURE);
+ return 0;
+}
+
+static void
+lua_plugin_push_ref (lua_State *L, struct lua_plugin *self, void *object,
+ struct ispect_field *field)
+{
+ // We create a mapping on object type registration
+ hard_assert (lua_rawgetp (L, LUA_REGISTRYINDEX, field->fields));
+ struct lua_weak_info *info = lua_touserdata (L, -1);
+ lua_pop (L, 1);
+
+ if (!field->is_list)
+ {
+ lua_weak_push (L, self, object, info);
+ return;
+ }
+
+ // As a rule in this codebase, these fields are right at the top of structs
+ struct list_header { LIST_HEADER (void) };
+
+ int i = 1;
+ lua_newtable (L);
+ LIST_FOR_EACH (struct list_header, iter, object)
+ {
+ lua_weak_push (L, self, iter, info);
+ lua_rawseti (L, -2, i++);
+ }
+}
+
+static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
+ const char *key, void *p, struct ispect_field *field);
+
+static void
+lua_plugin_push_struct (lua_State *L, struct lua_plugin *self,
+ enum ispect_type type, void *value, struct ispect_field *field)
+{
+ if (type == ISPECT_STR)
+ {
+ const struct str *s = value;
+ lua_pushlstring (L, s->str, s->len);
+ return;
+ }
+ if (type == ISPECT_STR_MAP)
+ {
+ struct str_map_iter iter = str_map_iter_make (value);
+
+ void *value;
+ lua_newtable (L);
+ while ((value = str_map_iter_next (&iter)))
+ lua_plugin_push_map_field (L, self, iter.link->key, value, field);
+ return;
+ }
+ hard_assert (!"unhandled introspection object type");
+}
+
+static void
+lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
+ const char *key, void *p, struct ispect_field *field)
+{
+ // That would mean maps in maps ad infinitum
+ hard_assert (field->subtype != ISPECT_STR_MAP);
+
+ intptr_t n = (intptr_t) p;
+ switch (field->subtype)
+ {
+ // Here the types are generally casted to a void pointer
+ case ISPECT_BOOL: lua_pushboolean (L, (bool ) n); break;
+ case ISPECT_INT: lua_pushinteger (L, (int ) n); break;
+ case ISPECT_UINT: lua_pushinteger (L, (unsigned ) n); break;
+ case ISPECT_SIZE: lua_pushinteger (L, (size_t ) n); break;
+ case ISPECT_STRING: lua_pushstring (L, p); break;
+ case ISPECT_REF: lua_plugin_push_ref (L, self, p, field); break;
+ default: lua_plugin_push_struct (L, self, field->subtype, p, field);
+ }
+ lua_setfield (L, -2, key);
+}
+
+static bool
+lua_plugin_property_get_ispect (lua_State *L, const char *property_name)
+{
+ struct lua_weak_info *info = lua_touserdata (L, lua_upvalueindex (1));
+ if (!info || !info->ispect)
+ return false;
+
+ struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
+ // TODO: I think we can do better than this, maybe binary search at least?
+ struct ispect_field *field;
+ for (field = info->ispect; field->name; field++)
+ if (!strcmp (property_name, field->name))
+ break;
+ if (!field->name)
+ return false;
+
+ struct lua_plugin *self = weak->plugin;
+ void *p = (uint8_t *) weak->object + field->offset;
+ switch (field->type)
+ {
+ // Here the types are really what's under the pointer
+ case ISPECT_BOOL: lua_pushboolean (L, *(bool *) p); break;
+ case ISPECT_INT: lua_pushinteger (L, *(int *) p); break;
+ case ISPECT_UINT: lua_pushinteger (L, *(unsigned *) p); break;
+ case ISPECT_SIZE: lua_pushinteger (L, *(size_t *) p); break;
+ case ISPECT_STRING: lua_pushstring (L, *(char **) p); break;
+ case ISPECT_REF: lua_plugin_push_ref (L, self, *(void **) p, field); break;
+ default: lua_plugin_push_struct (L, self, field->type, p, field);
+ }
+ return true;
+}
+
+static int
+lua_plugin_property_get (lua_State *L)
+{
+ luaL_checktype (L, 1, LUA_TUSERDATA);
+ const char *property_name = luaL_checkstring (L, 2);
+
+ // Either it's directly present in the metatable
+ if (luaL_getmetafield (L, 1, property_name))
+ return 1;
+
+ // Or we try to find and eventually call a getter method
+ char *getter_name = xstrdup_printf ("get_%s", property_name);
+ bool found = luaL_getmetafield (L, 1, getter_name);
+ free (getter_name);
+
+ if (found)
+ {
+ lua_pushvalue (L, 1);
+ lua_call (L, 1, 1);
+ return 1;
+ }
+
+ // Maybe we can find it via introspection
+ if (lua_plugin_property_get_ispect (L, property_name))
+ return 1;
+
+ // Or we look for a property set by the user (__gc cannot be overriden)
+ if (lua_getuservalue (L, 1) != LUA_TTABLE)
+ lua_pushnil (L);
+ else
+ lua_getfield (L, -1, property_name);
+ return 1;
+}
+
+static int
+lua_plugin_property_set (lua_State *L)
+{
+ luaL_checktype (L, 1, LUA_TUSERDATA);
+ const char *property_name = luaL_checkstring (L, 2);
+ luaL_checkany (L, 3);
+
+ // We use the associated value to store user-defined properties
+ int type = lua_getuservalue (L, 1);
+ if (type == LUA_TNIL)
+ {
+ lua_pop (L, 1);
+ lua_newtable (L);
+ lua_pushvalue (L, -1);
+ lua_setuservalue (L, 1);
+ }
+ else if (type != LUA_TTABLE)
+ return luaL_error (L, "associated value is not a table");
+
+ // Beware that we do not check for conflicts here;
+ // if Lua code writes a conflicting field, it is effectively ignored
+ lua_pushvalue (L, 3);
+ lua_setfield (L, -2, property_name);
+ return 0;
+}
+
+static void
+lua_plugin_add_accessors (lua_State *L, struct lua_weak_info *info)
+{
+ // Emulate properties for convenience
+ lua_pushlightuserdata (L, info);
+ lua_pushcclosure (L, lua_plugin_property_get, 1);
+ lua_setfield (L, -2, "__index");
+ lua_pushcfunction (L, lua_plugin_property_set);
+ lua_setfield (L, -2, "__newindex");
+}
+
+static void
+lua_plugin_reg_meta (lua_State *L, const char *name, luaL_Reg *fns)
+{
+ luaL_newmetatable (L, name);
+ luaL_setfuncs (L, fns, 0);
+ lua_plugin_add_accessors (L, NULL);
+ lua_pop (L, 1);
+}
+
+static void
+lua_plugin_reg_weak (lua_State *L, struct lua_weak_info *info, luaL_Reg *fns)
+{
+ // Create a mapping from the object type (info->ispect) back to metadata
+ // so that we can figure out what to create from ISPECT_REF fields
+ lua_pushlightuserdata (L, info);
+ lua_rawsetp (L, LUA_REGISTRYINDEX, info->ispect);
+
+ luaL_newmetatable (L, info->name);
+ luaL_setfuncs (L, fns, 0);
+ lua_plugin_add_accessors (L, info);
+ lua_pop (L, 1);
+}
+
+static struct plugin *
+lua_plugin_load (struct app_context *ctx, const char *filename,
+ struct error **e)
+{
+ lua_State *L = lua_newstate (lua_plugin_alloc, NULL);
+ if (!L)
+ {
+ error_set (e, "Lua initialization failed");
+ return NULL;
+ }
+
+ lua_atpanic (L, lua_plugin_panic);
+ luaL_openlibs (L);
+
+ struct lua_plugin *plugin = xcalloc (1, sizeof *plugin);
+ plugin->super.name = xstrdup (filename);
+ plugin->super.vtable = &lua_plugin_vtable;
+ plugin->ctx = ctx;
+ plugin->L = L;
+
+ luaL_checkversion (L);
+
+ // Register the xC library as a singleton with "plugin" as an upvalue
+ // (mostly historical, but rather convenient)
+ luaL_newmetatable (L, lua_ctx_info.name);
+ lua_pushlightuserdata (L, plugin);
+ luaL_setfuncs (L, lua_plugin_library, 1);
+ lua_plugin_add_accessors (L, &lua_ctx_info);
+
+ // Add the asynchronous library underneath
+ lua_newtable (L);
+ lua_pushlightuserdata (L, plugin);
+ luaL_setfuncs (L, lua_async_library, 1);
+ lua_setfield (L, -2, "async");
+ lua_pop (L, 1);
+
+ lua_weak_push (L, plugin, ctx, &lua_ctx_info);
+ lua_setglobal (L, lua_ctx_info.name);
+
+ // Create metatables for our objects
+ lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE, lua_hook_table);
+ lua_plugin_reg_weak (L, &lua_user_info, lua_user_table);
+ lua_plugin_reg_weak (L, &lua_channel_info, lua_channel_table);
+ lua_plugin_reg_weak (L, &lua_buffer_info, lua_buffer_table);
+ lua_plugin_reg_weak (L, &lua_server_info, lua_server_table);
+ lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_table);
+ lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
+
+ lua_plugin_reg_meta (L, XLUA_TASK_METATABLE, lua_task_table);
+ lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE, lua_wchannel_table);
+
+ struct error *error = NULL;
+ if (luaL_loadfile (L, filename))
+ error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
+ else if (!lua_plugin_call (plugin, 0, 0, &error))
+ {
+ error_set (e, "%s: %s", "Lua", error->message);
+ error_free (error);
+ }
+ else
+ return &plugin->super;
+
+ plugin_destroy (&plugin->super);
+ return NULL;
+}
+
+#endif // HAVE_LUA
+
+// --- Plugins -----------------------------------------------------------------
+
+typedef struct plugin *(*plugin_load_fn)
+ (struct app_context *ctx, const char *filename, struct error **e);
+
+// We can potentially add support for other scripting languages if so desired,
+// however this possibility is just a byproduct of abstraction
+static plugin_load_fn g_plugin_loaders[] =
+{
+#ifdef HAVE_LUA
+ lua_plugin_load,
+#endif // HAVE_LUA
+};
+
+static struct plugin *
+plugin_load_from_filename (struct app_context *ctx, const char *filename,
+ struct error **e)
+{
+ struct plugin *plugin = NULL;
+ struct error *error = NULL;
+ for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++)
+ if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error)
+ break;
+
+ if (error)
+ error_propagate (e, error);
+ else if (!plugin)
+ {
+ error_set (e, "no plugin handler for \"%s\"", filename);
+ return NULL;
+ }
+ return plugin;
+}
+
+static struct plugin *
+plugin_find (struct app_context *ctx, const char *name)
+{
+ LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
+ if (!strcmp (name, iter->name))
+ return iter;
+ return NULL;
+}
+
+static char *
+plugin_resolve_relative_filename (const char *filename)
+{
+ struct strv paths = strv_make ();
+ get_xdg_data_dirs (&paths);
+ strv_append (&paths, PROJECT_DATADIR);
+ char *result = resolve_relative_filename_generic
+ (&paths, PROGRAM_NAME "/plugins/", filename);
+ strv_free (&paths);
+ return result;
+}
+
+static struct plugin *
+plugin_load_by_name (struct app_context *ctx, const char *name,
+ struct error **e)
+{
+ struct plugin *plugin = plugin_find (ctx, name);
+ if (plugin)
+ {
+ error_set (e, "plugin already loaded");
+ return NULL;
+ }
+
+ // As a side effect, a plugin can be loaded multiple times by giving
+ // various relative or non-relative paths to the function; this is not
+ // supposed to be fool-proof though, that requires other mechanisms
+ char *filename = resolve_filename (name, plugin_resolve_relative_filename);
+ if (!filename)
+ {
+ error_set (e, "file not found");
+ return NULL;
+ }
+
+ plugin = plugin_load_from_filename (ctx, filename, e);
+ free (filename);
+ return plugin;
+}
+
+static void
+plugin_load (struct app_context *ctx, const char *name)
+{
+ struct error *e = NULL;
+ struct plugin *plugin = plugin_load_by_name (ctx, name, &e);
+ if (plugin)
+ {
+ // FIXME: this way the real name isn't available to the plugin on load,
+ // which has effect on e.g. plugin_config_name()
+ cstr_set (&plugin->name, xstrdup (name));
+
+ log_global_status (ctx, "Plugin \"#s\" loaded", name);
+ LIST_PREPEND (ctx->plugins, plugin);
+ }
+ else
+ {
+ log_global_error (ctx, "Can't load plugin \"#s\": #s",
+ name, e->message);
+ error_free (e);
+ }
+}
+
+static void
+plugin_unload (struct app_context *ctx, const char *name)
+{
+ struct plugin *plugin = plugin_find (ctx, name);
+ if (!plugin)
+ log_global_error (ctx, "Can't unload plugin \"#s\": #s",
+ name, "plugin not loaded");
+ else
+ {
+ log_global_status (ctx, "Plugin \"#s\" unloaded", name);
+ LIST_UNLINK (ctx->plugins, plugin);
+ plugin_destroy (plugin);
+ }
+}
+
+static void
+load_plugins (struct app_context *ctx)
+{
+ const char *plugins =
+ get_config_string (ctx->config.root, "general.plugin_autoload");
+ if (plugins)
+ {
+ struct strv v = strv_make ();
+ cstr_split (plugins, ",", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ plugin_load (ctx, v.vector[i]);
+ strv_free (&v);
+ }
+}
+
+// --- User input handling -----------------------------------------------------
+
+// HANDLER_NEEDS_REG is primarily for message sending commands,
+// as they may want to log buffer lines and use our current nickname
+
+enum handler_flags
+{
+ HANDLER_SERVER = (1 << 0), ///< Server context required
+ HANDLER_NEEDS_REG = (1 << 1), ///< Server registration required
+ HANDLER_CHANNEL_FIRST = (1 << 2), ///< Channel required, first argument
+ HANDLER_CHANNEL_LAST = (1 << 3) ///< Channel required, last argument
+};
+
+struct handler_args
+{
+ struct app_context *ctx; ///< Application context
+ struct buffer *buffer; ///< Current buffer
+ struct server *s; ///< Related server
+ const char *channel_name; ///< Related channel name
+ char *arguments; ///< Command arguments
+};
+
+/// Cuts the longest non-whitespace portion of text and advances the pointer
+static char *
+cut_word (char **s)
+{
+ char *start = *s;
+ size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);
+ char *end = start + word_len;
+ *s = end + strspn (end, WORD_BREAKING_CHARS);
+ *end = '\0';
+ return start;
+}
+
+/// Validates a word to be cut from a string
+typedef bool (*word_validator_fn) (void *, char *);
+
+static char *
+maybe_cut_word (char **s, word_validator_fn validator, void *user_data)
+{
+ char *start = *s;
+ size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);
+
+ char *word = xstrndup (start, word_len);
+ bool ok = validator (user_data, word);
+ free (word);
+
+ if (!ok)
+ return NULL;
+
+ char *end = start + word_len;
+ *s = end + strspn (end, WORD_BREAKING_CHARS);
+ *end = '\0';
+ return start;
+}
+
+static char *
+maybe_cut_word_from_end (char **s, word_validator_fn validator, void *user_data)
+{
+ // Find the start and end of the last word
+ // Contrary to maybe_cut_word(), we ignore all whitespace at the end
+ char *start = *s, *end = start + strlen (start);
+ while (end > start && strchr (WORD_BREAKING_CHARS, end [-1]))
+ end--;
+ char *word = end;
+ while (word > start && !strchr (WORD_BREAKING_CHARS, word[-1]))
+ word--;
+
+ // There's just one word at maximum, starting at the beginning
+ if (word == start)
+ return maybe_cut_word (s, validator, user_data);
+
+ char *tmp = xstrndup (word, word - start);
+ bool ok = validator (user_data, tmp);
+ free (tmp);
+
+ if (!ok)
+ return NULL;
+
+ // It doesn't start at the beginning, cut it off and return it
+ word[-1] = *end = '\0';
+ return word;
+}
+
+static bool
+validate_channel_name (void *user_data, char *word)
+{
+ return irc_is_channel (user_data, word);
+}
+
+static char *
+try_get_channel (struct handler_args *a,
+ char *(*cutter) (char **, word_validator_fn, void *))
+{
+ char *channel_name = cutter (&a->arguments, validate_channel_name, a->s);
+ if (channel_name)
+ return channel_name;
+ if (a->buffer->type == BUFFER_CHANNEL)
+ return a->buffer->channel->name;
+ return NULL;
+}
+
+static bool
+try_handle_buffer_goto (struct app_context *ctx, const char *word)
+{
+ unsigned long n;
+ if (!xstrtoul (&n, word, 10))
+ return false;
+
+ if (n > INT_MAX || !buffer_goto (ctx, n))
+ log_global_error (ctx, "#s: #s", "No such buffer", word);
+ return true;
+}
+
+static struct buffer *
+try_decode_buffer (struct app_context *ctx, const char *word)
+{
+ unsigned long n;
+ struct buffer *buffer = NULL;
+ if (xstrtoul (&n, word, 10) && n <= INT_MAX)
+ buffer = buffer_at_index (ctx, n);
+ if (buffer || (buffer = buffer_by_name (ctx, word)))
+ return buffer;
+
+ // Basic case insensitive partial matching -- at most one buffer can match
+ int n_matches = 0;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ {
+ char *string = xstrdup (iter->name);
+ char *pattern = xstrdup_printf ("*%s*", word);
+ for (char *p = string; *p; p++) *p = tolower_ascii (*p);
+ for (char *p = pattern; *p; p++) *p = tolower_ascii (*p);
+ if (!fnmatch (pattern, string, 0))
+ {
+ n_matches++;
+ buffer = iter;
+ }
+ free (string);
+ free (pattern);
+ }
+ return n_matches == 1 ? buffer : NULL;
+}
+
+static void
+show_buffers_list (struct app_context *ctx)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Buffers list:");
+
+ int i = 1;
+ LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
+ {
+ struct str s = str_make ();
+ int new = iter->new_messages_count - iter->new_unimportant_count;
+ if (new && iter != ctx->current_buffer)
+ str_append_printf (&s, " (%d%s)", new, &"!"[!iter->highlighted]);
+ log_global_indent (ctx,
+ " [#d] #s#&s", i++, iter->name, str_steal (&s));
+ }
+}
+
+static void
+part_channel (struct server *s, const char *channel_name, const char *reason)
+{
+ if (*reason)
+ irc_send (s, "PART %s :%s", channel_name, reason);
+ else
+ irc_send (s, "PART %s", channel_name);
+
+ struct channel *channel;
+ if ((channel = str_map_find (&s->irc_channels, channel_name)))
+ channel->left_manually = true;
+}
+
+static bool
+handle_buffer_goto (struct app_context *ctx, struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ const char *which = cut_word (&a->arguments);
+ struct buffer *buffer = try_decode_buffer (ctx, which);
+ if (buffer)
+ buffer_activate (ctx, buffer);
+ else
+ log_global_error (ctx, "#s: #s", "No such buffer", which);
+ return true;
+}
+
+static void
+handle_buffer_close (struct app_context *ctx, struct handler_args *a)
+{
+ struct buffer *buffer = NULL;
+ const char *which = NULL;
+ if (!*a->arguments)
+ buffer = a->buffer;
+ else
+ buffer = try_decode_buffer (ctx, (which = cut_word (&a->arguments)));
+
+ if (!buffer)
+ log_global_error (ctx, "#s: #s", "No such buffer", which);
+ else if (buffer == ctx->global_buffer)
+ log_global_error (ctx, "Can't close the global buffer");
+ else if (buffer->type == BUFFER_SERVER)
+ log_global_error (ctx, "Can't close a server buffer");
+ else
+ {
+ // The user would be unable to recreate the buffer otherwise
+ if (buffer->type == BUFFER_CHANNEL
+ && irc_channel_is_joined (buffer->channel))
+ part_channel (buffer->server, buffer->channel->name, "");
+ buffer_remove_safe (ctx, buffer);
+ }
+}
+
+static bool
+handle_buffer_move (struct app_context *ctx, struct handler_args *a)
+{
+ unsigned long request;
+ if (!xstrtoul (&request, a->arguments, 10))
+ return false;
+
+ if (request == 0 || request > (unsigned long) buffer_count (ctx))
+ {
+ log_global_error (ctx, "#s: #s",
+ "Can't move buffer", "requested position is out of range");
+ return true;
+ }
+ buffer_move (ctx, a->buffer, request);
+ return true;
+}
+
+static bool
+handle_command_buffer (struct handler_args *a)
+{
+ struct app_context *ctx = a->ctx;
+ char *action = cut_word (&a->arguments);
+ if (try_handle_buffer_goto (ctx, action))
+ return true;
+
+ bool result = true;
+ if (!*action || !strcasecmp_ascii (action, "list"))
+ show_buffers_list (ctx);
+ else if (!strcasecmp_ascii (action, "clear"))
+ {
+ buffer_clear (ctx, a->buffer);
+ if (a->buffer == ctx->current_buffer)
+ buffer_print_backlog (ctx, a->buffer);
+ }
+ else if (!strcasecmp_ascii (action, "move"))
+ result = handle_buffer_move (ctx, a);
+ else if (!strcasecmp_ascii (action, "goto"))
+ result = handle_buffer_goto (ctx, a);
+ else if (!strcasecmp_ascii (action, "close"))
+ handle_buffer_close (ctx, a);
+ else
+ result = false;
+ return result;
+}
+
+static bool
+handle_command_set_add
+ (struct strv *items, const struct strv *values, struct error **e)
+{
+ for (size_t i = 0; i < values->len; i++)
+ {
+ const char *value = values->vector[i];
+ if (strv_find (items, values->vector[i]) != -1)
+ return error_set (e, "already present in the array: %s", value);
+ strv_append (items, value);
+ }
+ return true;
+}
+
+static bool
+handle_command_set_remove
+ (struct strv *items, const struct strv *values, struct error **e)
+{
+ for (size_t i = 0; i < values->len; i++)
+ {
+ const char *value = values->vector[i];
+ ssize_t i = strv_find (items, value);
+ if (i == -1)
+ return error_set (e, "not present in the array: %s", value);
+ strv_remove (items, i);
+ }
+ return true;
+}
+
+static bool
+handle_command_set_modify
+ (struct config_item *item, const char *value, bool add, struct error **e)
+{
+ struct strv items = strv_make ();
+ if (item->type != CONFIG_ITEM_NULL)
+ cstr_split (item->value.string.str, ",", false, &items);
+ if (items.len == 1 && !*items.vector[0])
+ strv_reset (&items);
+
+ struct strv values = strv_make ();
+ cstr_split (value, ",", false, &values);
+ bool result = add
+ ? handle_command_set_add (&items, &values, e)
+ : handle_command_set_remove (&items, &values, e);
+
+ if (result)
+ {
+ char *changed = strv_join (&items, ",");
+ struct str tmp = { .str = changed, .len = strlen (changed) };
+ result = config_item_set_from (item,
+ config_item_string_array (&tmp), e);
+ free (changed);
+ }
+
+ strv_free (&items);
+ strv_free (&values);
+ return result;
+}
+
+static bool
+handle_command_set_assign_item (struct app_context *ctx,
+ char *key, struct config_item *new_, bool add, bool remove)
+{
+ struct config_item *item =
+ config_item_get (ctx->config.root, key, NULL);
+ hard_assert (item);
+
+ struct error *e = NULL;
+ if (!item->schema)
+ error_set (&e, "option not recognized");
+ else if (!add && !remove)
+ config_item_set_from (item, config_item_clone (new_), &e);
+ else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY)
+ error_set (&e, "not a string array");
+ else
+ handle_command_set_modify (item, new_->value.string.str, add, &e);
+
+ if (e)
+ {
+ log_global_error (ctx,
+ "Failed to set option \"#s\": #s", key, e->message);
+ error_free (e);
+ return false;
+ }
+
+ struct strv tmp = strv_make ();
+ dump_matching_options (ctx->config.root, key, &tmp);
+ log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
+ strv_free (&tmp);
+ return true;
+}
+
+static bool
+handle_command_set_assign
+ (struct app_context *ctx, struct strv *all, char *arguments)
+{
+ hard_assert (all->len > 0);
+
+ char *op = cut_word (&arguments);
+ bool add = false;
+ bool remove = false;
+
+ if (!strcmp (op, "+=")) add = true;
+ else if (!strcmp (op, "-=")) remove = true;
+ else if (strcmp (op, "=")) return false;
+
+ if (!*arguments)
+ return false;
+
+ struct error *e = NULL;
+ struct config_item *new_ =
+ config_item_parse (arguments, strlen (arguments), true, &e);
+ if (e)
+ {
+ log_global_error (ctx, "Invalid value: #s", e->message);
+ error_free (e);
+ return true;
+ }
+
+ if ((add | remove) && !config_item_type_is_string (new_->type))
+ {
+ log_global_error (ctx, "+= / -= operators need a string argument");
+ config_item_destroy (new_);
+ return true;
+ }
+
+ bool changed = false;
+ for (size_t i = 0; i < all->len; i++)
+ {
+ char *key = cstr_cut_until (all->vector[i], " ");
+ if (handle_command_set_assign_item (ctx, key, new_, add, remove))
+ changed = true;
+ free (key);
+ }
+ config_item_destroy (new_);
+
+ if (changed && get_config_boolean (ctx->config.root, "general.autosave"))
+ save_configuration (ctx);
+ return true;
+}
+
+static bool
+handle_command_set (struct handler_args *a)
+{
+ struct app_context *ctx = a->ctx;
+ char *option = "*";
+ if (*a->arguments)
+ option = cut_word (&a->arguments);
+
+ struct strv all = strv_make ();
+ dump_matching_options (ctx->config.root, option, &all);
+
+ bool result = true;
+ if (!all.len)
+ log_global_error (ctx, "No matches: #s", option);
+ else if (!*a->arguments)
+ {
+ log_global_indent (ctx, "");
+ for (size_t i = 0; i < all.len; i++)
+ log_global_indent (ctx, "#s", all.vector[i]);
+ }
+ else
+ result = handle_command_set_assign (ctx, &all, a->arguments);
+
+ strv_free (&all);
+ return result;
+}
+
+static bool
+handle_command_save (struct handler_args *a)
+{
+ if (*a->arguments)
+ return false;
+
+ save_configuration (a->ctx);
+ return true;
+}
+
+static void
+show_plugin_list (struct app_context *ctx)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Plugins:");
+ LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
+ log_global_indent (ctx, " #s", iter->name);
+}
+
+static bool
+handle_command_plugin (struct handler_args *a)
+{
+ char *action = cut_word (&a->arguments);
+ if (!*action || !strcasecmp_ascii (action, "list"))
+ show_plugin_list (a->ctx);
+ else if (!strcasecmp_ascii (action, "load"))
+ {
+ if (!*a->arguments)
+ return false;
+
+ plugin_load (a->ctx, cut_word (&a->arguments));
+ }
+ else if (!strcasecmp_ascii (action, "unload"))
+ {
+ if (!*a->arguments)
+ return false;
+
+ plugin_unload (a->ctx, cut_word (&a->arguments));
+ }
+ else
+ return false;
+ return true;
+}
+
+static bool
+handle_command_relay (struct handler_args *a)
+{
+ if (*a->arguments)
+ return false;
+
+ int len = 0;
+ LIST_FOR_EACH (struct client, c, a->ctx->clients)
+ len++;
+
+ if (a->ctx->relay_fd == -1)
+ log_global_status (a->ctx, "The relay is not enabled");
+ else
+ log_global_status (a->ctx, "The relay has #d clients", len);
+ return true;
+}
+
+static bool
+show_aliases_list (struct app_context *ctx)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Aliases:");
+
+ struct str_map *aliases = get_aliases_config (ctx);
+ if (!aliases->len)
+ {
+ log_global_indent (ctx, " (none)");
+ return true;
+ }
+
+ struct str_map_iter iter = str_map_iter_make (aliases);
+ struct config_item *alias;
+ while ((alias = str_map_iter_next (&iter)))
+ {
+ struct str definition = str_make ();
+ if (config_item_type_is_string (alias->type))
+ config_item_write_string (&definition, &alias->value.string);
+ else
+ str_append (&definition, "alias definition is not a string");
+ log_global_indent (ctx, " /#s: #s", iter.link->key, definition.str);
+ str_free (&definition);
+ }
+ return true;
+}
+
+static bool
+handle_command_alias (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return show_aliases_list (a->ctx);
+
+ char *name = cut_word (&a->arguments);
+ if (!*a->arguments)
+ return false;
+ if (*name == '/')
+ name++;
+
+ struct config_item *alias = config_item_string_from_cstr (a->arguments);
+
+ struct str definition = str_make ();
+ config_item_write_string (&definition, &alias->value.string);
+ str_map_set (get_aliases_config (a->ctx), name, alias);
+ log_global_status (a->ctx, "Created alias /#s: #s", name, definition.str);
+ str_free (&definition);
+ return true;
+}
+
+static bool
+handle_command_unalias (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ struct str_map *aliases = get_aliases_config (a->ctx);
+ while (*a->arguments)
+ {
+ char *name = cut_word (&a->arguments);
+ if (!str_map_find (aliases, name))
+ log_global_error (a->ctx, "No such alias: #s", name);
+ else
+ {
+ str_map_set (aliases, name, NULL);
+ log_global_status (a->ctx, "Alias removed: #s", name);
+ }
+ }
+ return true;
+}
+
+static bool
+handle_command_msg (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (!*a->arguments)
+ log_server_error (a->s, a->s->buffer, "No text to send");
+ else
+ SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
+ return true;
+}
+
+static bool
+handle_command_query (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (irc_is_channel (a->s, irc_skip_statusmsg (a->s, target)))
+ log_server_error (a->s, a->s->buffer, "Cannot query a channel");
+ else if (!*a->arguments)
+ buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
+ else
+ {
+ buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
+ SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
+ }
+ return true;
+}
+
+static bool
+handle_command_notice (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (!*a->arguments)
+ log_server_error (a->s, a->s->buffer, "No text to send");
+ else
+ SEND_AUTOSPLIT_NOTICE (a->s, target, a->arguments);
+ return true;
+}
+
+static bool
+handle_command_squery (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (!*a->arguments)
+ log_server_error (a->s, a->s->buffer, "No text to send");
+ else
+ irc_send (a->s, "SQUERY %s :%s", target, a->arguments);
+ return true;
+}
+
+static bool
+handle_command_ctcp (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (!*a->arguments)
+ return false;
+
+ char *tag = cut_word (&a->arguments);
+ cstr_transform (tag, toupper_ascii);
+
+ if (*a->arguments)
+ irc_send (a->s, "PRIVMSG %s :\x01%s %s\x01", target, tag, a->arguments);
+ else
+ irc_send (a->s, "PRIVMSG %s :\x01%s\x01", target, tag);
+ return true;
+}
+
+static bool
+handle_command_me (struct handler_args *a)
+{
+ if (a->buffer->type == BUFFER_CHANNEL)
+ SEND_AUTOSPLIT_ACTION (a->s,
+ a->buffer->channel->name, a->arguments);
+ else if (a->buffer->type == BUFFER_PM)
+ SEND_AUTOSPLIT_ACTION (a->s,
+ a->buffer->user->nickname, a->arguments);
+ else
+ log_server_error (a->s, a->s->buffer,
+ "Can't do this from a server buffer (#s)",
+ "send CTCP actions");
+ return true;
+}
+
+static bool
+handle_command_quit (struct handler_args *a)
+{
+ request_quit (a->ctx, *a->arguments ? a->arguments : NULL);
+ return true;
+}
+
+static bool
+handle_command_join (struct handler_args *a)
+{
+ // XXX: send the last known channel key?
+ if (irc_is_channel (a->s, a->arguments))
+ // XXX: we may want to split the list of channels
+ irc_send (a->s, "JOIN %s", a->arguments);
+ else if (a->buffer->type != BUFFER_CHANNEL)
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
+ "no channel name given and this buffer is not a channel");
+ else if (irc_channel_is_joined (a->buffer->channel))
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
+ "you already are on the channel");
+ else if (*a->arguments)
+ irc_send (a->s, "JOIN %s :%s", a->buffer->channel->name, a->arguments);
+ else
+ irc_send (a->s, "JOIN %s", a->buffer->channel->name);
+ return true;
+}
+
+static bool
+handle_command_part (struct handler_args *a)
+{
+ if (irc_is_channel (a->s, a->arguments))
+ {
+ struct strv v = strv_make ();
+ cstr_split (cut_word (&a->arguments), ",", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ part_channel (a->s, v.vector[i], a->arguments);
+ strv_free (&v);
+ }
+ else if (a->buffer->type != BUFFER_CHANNEL)
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
+ "no channel name given and this buffer is not a channel");
+ else if (!irc_channel_is_joined (a->buffer->channel))
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
+ "you're not on the channel");
+ else
+ part_channel (a->s, a->buffer->channel->name, a->arguments);
+ return true;
+}
+
+static void
+cycle_channel (struct server *s, const char *channel_name, const char *reason)
+{
+ // If a channel key is set, we must specify it when rejoining
+ const char *key = NULL;
+ struct channel *channel;
+ if ((channel = str_map_find (&s->irc_channels, channel_name)))
+ key = str_map_find (&channel->param_modes, "k");
+
+ if (*reason)
+ irc_send (s, "PART %s :%s", channel_name, reason);
+ else
+ irc_send (s, "PART %s", channel_name);
+
+ if (key)
+ irc_send (s, "JOIN %s :%s", channel_name, key);
+ else
+ irc_send (s, "JOIN %s", channel_name);
+}
+
+static bool
+handle_command_cycle (struct handler_args *a)
+{
+ if (irc_is_channel (a->s, a->arguments))
+ {
+ struct strv v = strv_make ();
+ cstr_split (cut_word (&a->arguments), ",", true, &v);
+ for (size_t i = 0; i < v.len; i++)
+ cycle_channel (a->s, v.vector[i], a->arguments);
+ strv_free (&v);
+ }
+ else if (a->buffer->type != BUFFER_CHANNEL)
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
+ "no channel name given and this buffer is not a channel");
+ else if (!irc_channel_is_joined (a->buffer->channel))
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
+ "you're not on the channel");
+ else
+ cycle_channel (a->s, a->buffer->channel->name, a->arguments);
+ return true;
+}
+
+static bool
+handle_command_mode (struct handler_args *a)
+{
+ // Channel names prefixed by "+" collide with mode strings,
+ // so we just disallow specifying these channels
+ char *target = NULL;
+ if (strchr ("+-\0", *a->arguments))
+ {
+ if (a->buffer->type == BUFFER_CHANNEL)
+ target = a->buffer->channel->name;
+ if (a->buffer->type == BUFFER_PM)
+ target = a->buffer->user->nickname;
+ if (a->buffer->type == BUFFER_SERVER)
+ target = a->s->irc_user->nickname;
+ }
+ else
+ // If there a->arguments and they don't begin with a mode string,
+ // they're either a user name or a channel name
+ target = cut_word (&a->arguments);
+
+ if (!target)
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't change mode",
+ "no target given and this buffer is neither a PM nor a channel");
+ else if (*a->arguments)
+ // XXX: split channel mode params as necessary using irc_max_modes?
+ irc_send (a->s, "MODE %s %s", target, a->arguments);
+ else
+ irc_send (a->s, "MODE %s", target);
+ return true;
+}
+
+static bool
+handle_command_topic (struct handler_args *a)
+{
+ if (*a->arguments)
+ // FIXME: there's no way to start the topic with whitespace
+ // FIXME: there's no way to unset the topic;
+ // we could adopt the Tcl style of "-switches" with "--" sentinels,
+ // or we could accept "strings" in the config format
+ irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments);
+ else
+ irc_send (a->s, "TOPIC %s", a->channel_name);
+ return true;
+}
+
+static bool
+handle_command_kick (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (*a->arguments)
+ irc_send (a->s, "KICK %s %s :%s",
+ a->channel_name, target, a->arguments);
+ else
+ irc_send (a->s, "KICK %s %s", a->channel_name, target);
+ return true;
+}
+
+static bool
+handle_command_kickban (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (strpbrk (target, "!@*?"))
+ return false;
+
+ // XXX: how about other masks?
+ irc_send (a->s, "MODE %s +b %s!*@*", a->channel_name, target);
+ if (*a->arguments)
+ irc_send (a->s, "KICK %s %s :%s",
+ a->channel_name, target, a->arguments);
+ else
+ irc_send (a->s, "KICK %s %s", a->channel_name, target);
+ return true;
+}
+
+static void
+mass_channel_mode (struct server *s, const char *channel_name,
+ bool adding, char mode_char, struct strv *v)
+{
+ size_t n;
+ for (size_t i = 0; i < v->len; i += n)
+ {
+ struct str modes = str_make ();
+ struct str params = str_make ();
+
+ n = MIN (v->len - i, s->irc_max_modes);
+ str_append_printf (&modes, "MODE %s %c", channel_name, "-+"[adding]);
+ for (size_t k = 0; k < n; k++)
+ {
+ str_append_c (&modes, mode_char);
+ str_append_printf (&params, " %s", v->vector[i + k]);
+ }
+
+ irc_send (s, "%s%s", modes.str, params.str);
+
+ str_free (&modes);
+ str_free (&params);
+ }
+}
+
+static void
+mass_channel_mode_mask_list
+ (struct handler_args *a, bool adding, char mode_char)
+{
+ struct strv v = strv_make ();
+ cstr_split (a->arguments, " ", true, &v);
+
+ // XXX: this may be a bit too trivial; we could also map nicknames
+ // to information from WHO polling or userhost-in-names
+ for (size_t i = 0; i < v.len; i++)
+ {
+ char *target = v.vector[i];
+ if (strpbrk (target, "!@*?") || irc_is_extban (a->s, target))
+ continue;
+
+ v.vector[i] = xstrdup_printf ("%s!*@*", target);
+ free (target);
+ }
+
+ mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
+ strv_free (&v);
+}
+
+static bool
+handle_command_ban (struct handler_args *a)
+{
+ if (*a->arguments)
+ mass_channel_mode_mask_list (a, true, 'b');
+ else
+ irc_send (a->s, "MODE %s +b", a->channel_name);
+ return true;
+}
+
+static bool
+handle_command_unban (struct handler_args *a)
+{
+ if (*a->arguments)
+ mass_channel_mode_mask_list (a, false, 'b');
+ else
+ return false;
+ return true;
+}
+
+static bool
+handle_command_invite (struct handler_args *a)
+{
+ struct strv v = strv_make ();
+ cstr_split (a->arguments, " ", true, &v);
+
+ bool result = !!v.len;
+ for (size_t i = 0; i < v.len; i++)
+ irc_send (a->s, "INVITE %s %s", v.vector[i], a->channel_name);
+
+ strv_free (&v);
+ return result;
+}
+
+static struct server *
+resolve_server (struct app_context *ctx, struct handler_args *a,
+ const char *command_name)
+{
+ struct server *s = NULL;
+ if (*a->arguments)
+ {
+ char *server_name = cut_word (&a->arguments);
+ if (!(s = str_map_find (&ctx->servers, server_name)))
+ log_global_error (ctx, "/#s: #s: #s",
+ command_name, "no such server", server_name);
+ }
+ else if (a->buffer->type == BUFFER_GLOBAL)
+ log_global_error (ctx, "/#s: #s",
+ command_name, "no server name given and this buffer is global");
+ else
+ s = a->buffer->server;
+ return s;
+}
+
+static bool
+handle_command_connect (struct handler_args *a)
+{
+ struct server *s = NULL;
+ if (!(s = resolve_server (a->ctx, a, "connect")))
+ return true;
+
+ if (irc_is_connected (s))
+ {
+ log_server_error (s, s->buffer, "Already connected");
+ return true;
+ }
+ if (s->state == IRC_CONNECTING)
+ irc_destroy_connector (s);
+
+ irc_cancel_timers (s);
+
+ s->reconnect_attempt = 0;
+ irc_initiate_connect (s);
+ return true;
+}
+
+static bool
+handle_command_disconnect (struct handler_args *a)
+{
+ struct server *s = NULL;
+ if (!(s = resolve_server (a->ctx, a, "disconnect")))
+ return true;
+
+ if (s->state == IRC_CONNECTING)
+ {
+ log_server_status (s, s->buffer, "Connecting aborted");
+ irc_destroy_connector (s);
+ }
+ else if (poller_timer_is_active (&s->reconnect_tmr))
+ {
+ log_server_status (s, s->buffer, "Connecting aborted");
+ poller_timer_reset (&s->reconnect_tmr);
+ }
+ else if (!irc_is_connected (s))
+ log_server_error (s, s->buffer, "Not connected");
+ else
+ irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
+ return true;
+}
+
+static bool
+show_servers_list (struct app_context *ctx)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Servers list:");
+
+ struct str_map_iter iter = str_map_iter_make (&ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ log_global_indent (ctx, " #s", s->name);
+ return true;
+}
+
+static bool
+handle_server_add (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ struct app_context *ctx = a->ctx;
+ char *name = cut_word (&a->arguments);
+ const char *err;
+ if ((err = check_server_name_for_addition (ctx, name)))
+ log_global_error (ctx, "Cannot create server `#s': #s", name, err);
+ else
+ {
+ server_add_new (ctx, name);
+ log_global_status (ctx, "Server added: #s", name);
+ }
+ return true;
+}
+
+static bool
+handle_server_remove (struct handler_args *a)
+{
+ struct app_context *ctx = a->ctx;
+ struct server *s = NULL;
+ if (!(s = resolve_server (ctx, a, "server")))
+ return true;
+
+ if (irc_is_connected (s))
+ log_server_error (s, s->buffer, "Can't remove a connected server");
+ else
+ {
+ char *name = xstrdup (s->name);
+ server_remove (ctx, s);
+ log_global_status (ctx, "Server removed: #s", name);
+ free (name);
+ }
+ return true;
+}
+
+static bool
+handle_server_rename (struct handler_args *a)
+{
+ struct app_context *ctx = a->ctx;
+ if (!*a->arguments)
+ return false;
+ char *old_name = cut_word (&a->arguments);
+ if (!*a->arguments)
+ return false;
+ char *new_name = cut_word (&a->arguments);
+
+ struct server *s;
+ const char *err;
+ if (!(s = str_map_find (&ctx->servers, old_name)))
+ log_global_error (ctx, "/#s: #s: #s",
+ "server", "no such server", old_name);
+ else if ((err = check_server_name_for_addition (ctx, new_name)))
+ log_global_error (ctx,
+ "Cannot rename server to `#s': #s", new_name, err);
+ else
+ {
+ server_rename (ctx, s, new_name);
+ log_global_status (ctx, "Server renamed: #s to #s", old_name, new_name);
+ }
+ return true;
+}
+
+static bool
+handle_command_server (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return show_servers_list (a->ctx);
+
+ char *action = cut_word (&a->arguments);
+ if (!strcasecmp_ascii (action, "list"))
+ return show_servers_list (a->ctx);
+ if (!strcasecmp_ascii (action, "add"))
+ return handle_server_add (a);
+ if (!strcasecmp_ascii (action, "remove"))
+ return handle_server_remove (a);
+ if (!strcasecmp_ascii (action, "rename"))
+ return handle_server_rename (a);
+ return false;
+}
+
+static bool
+handle_command_names (struct handler_args *a)
+{
+ char *channel_name = try_get_channel (a, maybe_cut_word);
+ if (channel_name)
+ irc_send (a->s, "NAMES %s", channel_name);
+ else
+ irc_send (a->s, "NAMES");
+ return true;
+}
+
+static bool
+handle_command_whois (struct handler_args *a)
+{
+ if (*a->arguments)
+ irc_send (a->s, "WHOIS %s", a->arguments);
+ else if (a->buffer->type == BUFFER_PM)
+ irc_send (a->s, "WHOIS %s", a->buffer->user->nickname);
+ else if (a->buffer->type == BUFFER_SERVER)
+ irc_send (a->s, "WHOIS %s", a->s->irc_user->nickname);
+ else
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
+ "no target given and this buffer is neither a PM nor a server");
+ return true;
+}
+
+static bool
+handle_command_whowas (struct handler_args *a)
+{
+ if (*a->arguments)
+ irc_send (a->s, "WHOWAS %s", a->arguments);
+ else if (a->buffer->type == BUFFER_PM)
+ irc_send (a->s, "WHOWAS %s", a->buffer->user->nickname);
+ else
+ log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
+ "no target given and this buffer is not a PM");
+ return true;
+}
+
+static bool
+handle_command_kill (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ char *target = cut_word (&a->arguments);
+ if (*a->arguments)
+ irc_send (a->s, "KILL %s :%s", target, a->arguments);
+ else
+ irc_send (a->s, "KILL %s", target);
+ return true;
+}
+
+static bool
+handle_command_nick (struct handler_args *a)
+{
+ if (!*a->arguments)
+ return false;
+
+ irc_send (a->s, "NICK %s", cut_word (&a->arguments));
+ return true;
+}
+
+static bool
+handle_command_quote (struct handler_args *a)
+{
+ irc_send (a->s, "%s", a->arguments);
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+handle_command_channel_mode
+ (struct handler_args *a, bool adding, char mode_char)
+{
+ const char *targets = a->arguments;
+ if (!*targets)
+ {
+ if (adding)
+ return false;
+
+ targets = a->s->irc_user->nickname;
+ }
+
+ struct strv v = strv_make ();
+ cstr_split (targets, " ", true, &v);
+ mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
+ strv_free (&v);
+ return true;
+}
+
+#define CHANMODE_HANDLER(name, adding, mode_char) \
+ static bool \
+ handle_command_ ## name (struct handler_args *a) \
+ { \
+ return handle_command_channel_mode (a, (adding), (mode_char)); \
+ }
+
+CHANMODE_HANDLER (op, true, 'o') CHANMODE_HANDLER (deop, false, 'o')
+CHANMODE_HANDLER (voice, true, 'v') CHANMODE_HANDLER (devoice, false, 'v')
+
+#define TRIVIAL_HANDLER(name, command) \
+ static bool \
+ handle_command_ ## name (struct handler_args *a) \
+ { \
+ if (*a->arguments) \
+ irc_send (a->s, command " %s", a->arguments); \
+ else \
+ irc_send (a->s, command); \
+ return true; \
+ }
+
+TRIVIAL_HANDLER (list, "LIST")
+TRIVIAL_HANDLER (who, "WHO")
+TRIVIAL_HANDLER (motd, "MOTD")
+TRIVIAL_HANDLER (oper, "OPER")
+TRIVIAL_HANDLER (stats, "STATS")
+TRIVIAL_HANDLER (away, "AWAY")
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool handle_command_help (struct handler_args *);
+
+static struct command_handler
+{
+ const char *name;
+ const char *description;
+ const char *usage;
+ bool (*handler) (struct handler_args *a);
+ enum handler_flags flags;
+}
+g_command_handlers[] =
+{
+ { "help", "Show help",
+ "[<command> | <option>]",
+ handle_command_help, 0 },
+ { "quit", "Quit the program",
+ "[<message>]",
+ handle_command_quit, 0 },
+ { "buffer", "Manage buffers",
+ "<N> | list | clear | move <N> | goto <N or name> | close [<N or name>]",
+ handle_command_buffer, 0 },
+ { "set", "Manage configuration",
+ "[<option>]",
+ handle_command_set, 0 },
+ { "save", "Save configuration",
+ NULL,
+ handle_command_save, 0 },
+ { "plugin", "Manage plugins",
+ "list | load <name> | unload <name>",
+ handle_command_plugin, 0 },
+ { "relay", "Show relay information",
+ NULL,
+ handle_command_relay, 0 },
+
+ { "alias", "List or set aliases",
+ "[<name> <definition>]",
+ handle_command_alias, 0 },
+ { "unalias", "Unset aliases",
+ "<name>...",
+ handle_command_unalias, 0 },
+
+ { "msg", "Send message to a nick or channel",
+ "<target> <message>",
+ handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "query", "Send a private message to a nick",
+ "<nick> <message>",
+ handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "notice", "Send notice to a nick or channel",
+ "<target> <message>",
+ handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "squery", "Send a message to a service",
+ "<service> <message>",
+ handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "ctcp", "Send a CTCP query",
+ "<target> <tag>",
+ handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "me", "Send a CTCP action",
+ "<message>",
+ handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG },
+
+ { "join", "Join channels",
+ "[<channel>[,<channel>...]] [<key>[,<key>...]]",
+ handle_command_join, HANDLER_SERVER },
+ { "part", "Leave channels",
+ "[<channel>[,<channel>...]] [<reason>]",
+ handle_command_part, HANDLER_SERVER },
+ { "cycle", "Rejoin channels",
+ "[<channel>[,<channel>...]] [<reason>]",
+ handle_command_cycle, HANDLER_SERVER },
+
+ { "op", "Give channel operator status",
+ "<nick>...",
+ handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "deop", "Remove channel operator status",
+ "[<nick>...]",
+ handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "voice", "Give voice",
+ "<nick>...",
+ handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "devoice", "Remove voice",
+ "[<nick>...]",
+ handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+
+ { "mode", "Change mode",
+ "[<channel>] [<mode>...]",
+ handle_command_mode, HANDLER_SERVER },
+ { "topic", "Change topic",
+ "[<channel>] [<topic>]",
+ handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "kick", "Kick user from channel",
+ "[<channel>] <user> [<reason>]",
+ handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "kickban", "Kick and ban user from channel",
+ "[<channel>] <user> [<reason>]",
+ handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "ban", "Ban user from channel",
+ "[<channel>] [<mask>...]",
+ handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "unban", "Unban user from channel",
+ "[<channel>] <mask>...",
+ handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "invite", "Invite user to channel",
+ "<user>... [<channel>]",
+ handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST },
+
+ { "server", "Manage servers",
+ "list | add <name> | remove <name> | rename <old> <new>",
+ handle_command_server, 0 },
+ { "connect", "Connect to the server",
+ "[<server>]",
+ handle_command_connect, 0 },
+ { "disconnect", "Disconnect from the server",
+ "[<server> [<reason>]]",
+ handle_command_disconnect, 0 },
+
+ { "list", "List channels and their topic",
+ "[<channel>[,<channel>...]] [<target>]",
+ handle_command_list, HANDLER_SERVER },
+ { "names", "List users on channel",
+ "[<channel>[,<channel>...]]",
+ handle_command_names, HANDLER_SERVER },
+ { "who", "List users",
+ "[<mask> [o]]",
+ handle_command_who, HANDLER_SERVER },
+ { "whois", "Get user information",
+ "[<target>] <mask>",
+ handle_command_whois, HANDLER_SERVER },
+ { "whowas", "Get user information",
+ "<user> [<count> [<target>]]",
+ handle_command_whowas, HANDLER_SERVER },
+
+ { "motd", "Get the Message of The Day",
+ "[<target>]",
+ handle_command_motd, HANDLER_SERVER },
+ { "oper", "Authenticate as an IRC operator",
+ "<name> <password>",
+ handle_command_oper, HANDLER_SERVER },
+ { "kill", "Kick another user from the server",
+ "<user> <comment>",
+ handle_command_kill, HANDLER_SERVER },
+ { "stats", "Query server statistics",
+ "[<query> [<target>]]",
+ handle_command_stats, HANDLER_SERVER },
+ { "away", "Set away status",
+ "[<text>]",
+ handle_command_away, HANDLER_SERVER },
+ { "nick", "Change current nick",
+ "<nickname>",
+ handle_command_nick, HANDLER_SERVER },
+ { "quote", "Send a raw command to the server",
+ "<command>",
+ handle_command_quote, HANDLER_SERVER },
+};
+
+static bool
+try_handle_command_help_option (struct app_context *ctx, const char *name)
+{
+ struct config_item *item =
+ config_item_get (ctx->config.root, name, NULL);
+ if (!item)
+ return false;
+
+ struct config_schema *schema = item->schema;
+ if (!schema)
+ {
+ log_global_error (ctx, "#s: #s", "Option not recognized", name);
+ return true;
+ }
+
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Option \"#s\":", name);
+ log_global_indent (ctx, " Description: #s",
+ schema->comment ? schema->comment : "(none)");
+ log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type));
+ log_global_indent (ctx, " Default: #s",
+ schema->default_ ? schema->default_ : "null");
+
+ struct str tmp = str_make ();
+ config_item_write (item, false, &tmp);
+ log_global_indent (ctx, " Current value: #s", tmp.str);
+ str_free (&tmp);
+ return true;
+}
+
+static bool
+show_command_list (struct app_context *ctx)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "Commands:");
+
+ int longest = 0;
+ for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
+ {
+ int len = strlen (g_command_handlers[i].name);
+ longest = MAX (longest, len);
+ }
+ for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
+ {
+ struct command_handler *handler = &g_command_handlers[i];
+ log_global_indent (ctx, " #&s", xstrdup_printf
+ ("%-*s %s", longest, handler->name, handler->description));
+ }
+ return true;
+}
+
+static bool
+show_command_help (struct app_context *ctx, struct command_handler *handler)
+{
+ log_global_indent (ctx, "");
+ log_global_indent (ctx, "/#s: #s", handler->name, handler->description);
+ log_global_indent (ctx, " Arguments: #s",
+ handler->usage ? handler->usage : "(none)");
+ return true;
+}
+
+static bool
+handle_command_help (struct handler_args *a)
+{
+ struct app_context *ctx = a->ctx;
+ if (!*a->arguments)
+ return show_command_list (ctx);
+
+ const char *word = cut_word (&a->arguments);
+
+ const char *command = word + (*word == '/');
+ for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
+ {
+ struct command_handler *handler = &g_command_handlers[i];
+ if (!strcasecmp_ascii (command, handler->name))
+ return show_command_help (ctx, handler);
+ }
+
+ if (try_handle_command_help_option (ctx, word))
+ return true;
+
+ if (str_map_find (get_aliases_config (ctx), command))
+ log_global_status (ctx, "/#s is an alias", command);
+ else
+ log_global_error (ctx, "#s: #s", "No such command or option", word);
+ return true;
+}
+
+static void
+init_user_command_map (struct str_map *map)
+{
+ *map = str_map_make (NULL);
+ map->key_xfrm = tolower_ascii_strxfrm;
+
+ for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
+ {
+ struct command_handler *handler = &g_command_handlers[i];
+ str_map_set (map, handler->name, handler);
+ }
+}
+
+static bool
+process_user_command (struct app_context *ctx, struct buffer *buffer,
+ const char *command_name, char *input)
+{
+ static bool initialized = false;
+ static struct str_map map;
+ if (!initialized)
+ {
+ init_user_command_map (&map);
+ initialized = true;
+ }
+
+ if (try_handle_buffer_goto (ctx, command_name))
+ return true;
+
+ struct handler_args args =
+ {
+ .ctx = ctx,
+ .buffer = buffer,
+ .arguments = input,
+ };
+
+ struct command_handler *handler;
+ if (!(handler = str_map_find (&map, command_name)))
+ return false;
+ hard_assert (handler->flags == 0 || (handler->flags & HANDLER_SERVER));
+
+ if ((handler->flags & HANDLER_SERVER)
+ && args.buffer->type == BUFFER_GLOBAL)
+ log_global_error (ctx, "/#s: #s",
+ command_name, "can't do this from a global buffer");
+ else if ((handler->flags & HANDLER_SERVER)
+ && !irc_is_connected ((args.s = args.buffer->server)))
+ log_server_error (args.s, args.s->buffer, "Not connected");
+ else if ((handler->flags & HANDLER_NEEDS_REG)
+ && args.s->state != IRC_REGISTERED)
+ log_server_error (args.s, args.s->buffer, "Not registered");
+ else if (((handler->flags & HANDLER_CHANNEL_FIRST)
+ && !(args.channel_name =
+ try_get_channel (&args, maybe_cut_word)))
+ || ((handler->flags & HANDLER_CHANNEL_LAST)
+ && !(args.channel_name =
+ try_get_channel (&args, maybe_cut_word_from_end))))
+ log_server_error (args.s, args.buffer, "/#s: #s", command_name,
+ "no channel name given and this buffer is not a channel");
+ else if (!handler->handler (&args))
+ log_global_error (ctx,
+ "#s: /#s #s", "Usage", handler->name, handler->usage);
+ return true;
+}
+
+static const char *
+expand_alias_escape (const char *p, const char *arguments, struct str *output)
+{
+ struct strv words = strv_make ();
+ cstr_split (arguments, " ", true, &words);
+
+ // TODO: eventually also add support for argument ranges
+ // - Can use ${0}, ${0:}, ${:0}, ${1:-1} with strtol, dispose of $1 syntax
+ // (default aliases don't use numeric arguments).
+ // - Start numbering from zero, since we'd have to figure out what to do
+ // in case we encounter a zero if we keep the current approach.
+ // - Ignore the sequence altogether if no closing '}' can be found,
+ // or if the internal format doesn't fit the above syntax.
+ if (*p >= '1' && *p <= '9')
+ {
+ size_t offset = *p - '1';
+ if (offset < words.len)
+ str_append (output, words.vector[offset]);
+ }
+ else if (*p == '*')
+ str_append (output, arguments);
+ else if (strchr ("$;", *p))
+ str_append_c (output, *p);
+ else
+ str_append_printf (output, "$%c", *p);
+
+ strv_free (&words);
+ return ++p;
+}
+
+static void
+expand_alias_definition (const char *definition, const char *arguments,
+ struct strv *commands)
+{
+ struct str expanded = str_make ();
+ bool escape = false;
+ for (const char *p = definition; *p; p++)
+ {
+ if (escape)
+ {
+ p = expand_alias_escape (p, arguments, &expanded) - 1;
+ escape = false;
+ }
+ else if (*p == ';')
+ {
+ strv_append_owned (commands, str_steal (&expanded));
+ expanded = str_make ();
+ }
+ else if (*p == '$' && p[1])
+ escape = true;
+ else
+ str_append_c (&expanded, *p);
+ }
+ strv_append_owned (commands, str_steal (&expanded));
+}
+
+static bool
+expand_alias (struct app_context *ctx,
+ const char *alias_name, char *input, struct strv *commands)
+{
+ struct config_item *entry =
+ str_map_find (get_aliases_config (ctx), alias_name);
+ if (!entry)
+ return false;
+
+ if (!config_item_type_is_string (entry->type))
+ {
+ log_global_error (ctx, "Error executing `/#s': #s",
+ alias_name, "alias definition is not a string");
+ return false;
+ }
+
+ expand_alias_definition (entry->value.string.str, input, commands);
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+send_message_to_target (struct server *s,
+ const char *target, char *message, struct buffer *buffer)
+{
+ if (!irc_is_connected (s))
+ log_server_error (s, buffer, "Not connected");
+ else
+ SEND_AUTOSPLIT_PRIVMSG (s, target, message);
+}
+
+static void
+send_message_to_buffer (struct app_context *ctx, struct buffer *buffer,
+ char *message)
+{
+ hard_assert (buffer != NULL);
+
+ switch (buffer->type)
+ {
+ case BUFFER_CHANNEL:
+ send_message_to_target (buffer->server,
+ buffer->channel->name, message, buffer);
+ break;
+ case BUFFER_PM:
+ send_message_to_target (buffer->server,
+ buffer->user->nickname, message, buffer);
+ break;
+ default:
+ log_full (ctx, NULL, buffer, 0, BUFFER_LINE_ERROR,
+ "This buffer is not a channel");
+ }
+}
+
+static bool
+process_alias (struct app_context *ctx, struct buffer *buffer,
+ struct strv *commands, int level)
+{
+ for (size_t i = 0; i < commands->len; i++)
+ log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
+ (int) i, commands->vector[i]);
+ for (size_t i = 0; i < commands->len; i++)
+ if (!process_input_line (ctx, buffer, commands->vector[i], ++level))
+ return false;
+ return true;
+}
+
+static bool
+process_input_line_posthook (struct app_context *ctx, struct buffer *buffer,
+ char *input, int alias_level)
+{
+ if (*input != '/' || *++input == '/')
+ {
+ send_message_to_buffer (ctx, buffer, input);
+ return true;
+ }
+
+ char *name = cut_word (&input);
+ if (process_user_command (ctx, buffer, name, input))
+ return true;
+
+ struct strv commands = strv_make ();
+ bool result = false;
+ if (!expand_alias (ctx, name, input, &commands))
+ log_global_error (ctx, "#s: /#s", "No such command or alias", name);
+ else if (alias_level != 0)
+ log_global_error (ctx, "#s: /#s", "Aliases can't nest", name);
+ else
+ result = process_alias (ctx, buffer, &commands, alias_level);
+
+ strv_free (&commands);
+ return result;
+}
+
+static char *
+process_input_hooks (struct app_context *ctx, struct buffer *buffer,
+ char *input)
+{
+ uint64_t hash = siphash_wrapper (input, strlen (input));
+ LIST_FOR_EACH (struct hook, iter, ctx->input_hooks)
+ {
+ struct input_hook *hook = (struct input_hook *) iter;
+ if (!(input = hook->filter (hook, buffer, input)))
+ {
+ log_global_debug (ctx, "Input thrown away by hook");
+ return NULL;
+ }
+
+ uint64_t new_hash = siphash_wrapper (input, strlen (input));
+ if (new_hash != hash)
+ log_global_debug (ctx, "Input transformed to \"#s\"#r", input);
+ hash = new_hash;
+ }
+ return input;
+}
+
+static bool
+process_input_line (struct app_context *ctx, struct buffer *buffer,
+ const char *input, int alias_level)
+{
+ // Note that this also gets called on expanded aliases,
+ // which might or might not be desirable (we can forward "alias_level")
+ char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
+ bool result = !processed
+ || process_input_line_posthook (ctx, buffer, processed, alias_level);
+ free (processed);
+ return result;
+}
+
+static void
+process_input (struct app_context *ctx, struct buffer *buffer,
+ const char *input)
+{
+ struct strv lines = strv_make ();
+ cstr_split (input, "\r\n", false, &lines);
+ for (size_t i = 0; i < lines.len; i++)
+ (void) process_input_line (ctx, buffer, lines.vector[i], 0);
+ strv_free (&lines);
+}
+
+// --- Word completion ---------------------------------------------------------
+
+// The amount of crap that goes into this is truly insane.
+// It's mostly because of Editline's total ignorance of this task.
+
+static void
+completion_free (struct completion *self)
+{
+ free (self->line);
+ free (self->words);
+}
+
+static void
+completion_add_word (struct completion *self, size_t start, size_t end)
+{
+ if (!self->words)
+ self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
+ if (self->words_len == self->words_alloc)
+ self->words = xreallocarray (self->words,
+ (self->words_alloc <<= 1), sizeof *self->words);
+ self->words[self->words_len++] = (struct completion_word) { start, end };
+}
+
+static struct completion
+completion_make (const char *line, size_t len)
+{
+ struct completion self = { .line = xstrndup (line, len) };
+
+ // The first and the last word may be empty
+ const char *s = self.line;
+ while (true)
+ {
+ const char *start = s;
+ size_t word_len = strcspn (s, WORD_BREAKING_CHARS);
+ const char *end = start + word_len;
+ s = end + strspn (end, WORD_BREAKING_CHARS);
+
+ completion_add_word (&self, start - self.line, end - self.line);
+ if (s == end)
+ break;
+ }
+ return self;
+}
+
+static void
+completion_locate (struct completion *self, size_t offset)
+{
+ size_t i = 0;
+ for (; i < self->words_len; i++)
+ if (self->words[i].start > offset)
+ break;
+ self->location = i - 1;
+}
+
+static char *
+completion_word (struct completion *self, int word)
+{
+ hard_assert (word >= 0 && word < (int) self->words_len);
+ return xstrndup (self->line + self->words[word].start,
+ self->words[word].end - self->words[word].start);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// XXX: this isn't completely right because Unicode, but let's keep it simple.
+// At worst it will stop before a combining mark, or fail to compare
+// non-ASCII identifiers case-insensitively.
+
+static size_t
+utf8_common_prefix (const char **vector, size_t len)
+{
+ size_t prefix = 0;
+ if (!vector || !len)
+ return 0;
+
+ struct utf8_iter a[len];
+ for (size_t i = 0; i < len; i++)
+ a[i] = utf8_iter_make (vector[i]);
+
+ size_t ch_len;
+ int32_t ch;
+ while ((ch = utf8_iter_next (&a[0], &ch_len)) >= 0)
+ {
+ for (size_t i = 1; i < len; i++)
+ {
+ int32_t other = utf8_iter_next (&a[i], NULL);
+ if (ch == other)
+ continue;
+ // Not bothering with lowercasing non-ASCII
+ if (ch >= 0x80 || other >= 0x80
+ || tolower_ascii (ch) != tolower_ascii (other))
+ return prefix;
+ }
+ prefix += ch_len;
+ }
+ return prefix;
+}
+
+static void
+complete_command (struct app_context *ctx, struct completion *data,
+ const char *word, struct strv *output)
+{
+ (void) data;
+
+ const char *prefix = "";
+ if (*word == '/')
+ {
+ word++;
+ prefix = "/";
+ }
+
+ size_t word_len = strlen (word);
+ for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
+ {
+ struct command_handler *handler = &g_command_handlers[i];
+ if (!strncasecmp_ascii (word, handler->name, word_len))
+ strv_append_owned (output,
+ xstrdup_printf ("%s%s", prefix, handler->name));
+ }
+
+ struct str_map_iter iter = str_map_iter_make (get_aliases_config (ctx));
+ struct config_item *alias;
+ while ((alias = str_map_iter_next (&iter)))
+ {
+ if (!strncasecmp_ascii (word, iter.link->key, word_len))
+ strv_append_owned (output,
+ xstrdup_printf ("%s%s", prefix, iter.link->key));
+ }
+}
+
+static void
+complete_option (struct app_context *ctx, struct completion *data,
+ const char *word, struct strv *output)
+{
+ (void) data;
+
+ struct strv options = strv_make ();
+ config_dump (ctx->config.root, &options);
+ strv_sort (&options);
+
+ // Wildcard expansion is an interesting side-effect
+ char *mask = xstrdup_printf ("%s*", word);
+ for (size_t i = 0; i < options.len; i++)
+ {
+ char *key = cstr_cut_until (options.vector[i], " ");
+ if (!fnmatch (mask, key, 0))
+ strv_append_owned (output, key);
+ else
+ free (key);
+ }
+ free (mask);
+ strv_free (&options);
+}
+
+static void
+complete_set_value (struct config_item *item, const char *word,
+ struct strv *output)
+{
+ struct str serialized = str_make ();
+ config_item_write (item, false, &serialized);
+ if (!strncmp (serialized.str, word, strlen (word)))
+ strv_append_owned (output, str_steal (&serialized));
+ else
+ str_free (&serialized);
+}
+
+static void
+complete_set_value_array (struct config_item *item, const char *word,
+ struct strv *output)
+{
+ if (!item->schema || item->schema->type != CONFIG_ITEM_STRING_ARRAY)
+ return;
+
+ struct strv items = strv_make ();
+ cstr_split (item->value.string.str, ",", false, &items);
+ for (size_t i = 0; i < items.len; i++)
+ {
+ struct str wrapped = str_from_cstr (items.vector[i]);
+ struct str serialized = str_make ();
+ config_item_write_string (&serialized, &wrapped);
+ str_free (&wrapped);
+
+ if (!strncmp (serialized.str, word, strlen (word)))
+ strv_append_owned (output, str_steal (&serialized));
+ else
+ str_free (&serialized);
+ }
+ strv_free (&items);
+}
+
+static void
+complete_set (struct app_context *ctx, struct completion *data,
+ const char *word, struct strv *output)
+{
+ if (data->location == 1)
+ {
+ complete_option (ctx, data, word, output);
+ return;
+ }
+ if (data->location != 3)
+ return;
+
+ char *key = completion_word (data, 1);
+ struct config_item *item = config_item_get (ctx->config.root, key, NULL);
+ if (item)
+ {
+ char *op = completion_word (data, 2);
+ if (!strcmp (op, "-=")) complete_set_value_array (item, word, output);
+ if (!strcmp (op, "=")) complete_set_value (item, word, output);
+ free (op);
+ }
+ free (key);
+}
+
+static void
+complete_topic (struct buffer *buffer, struct completion *data,
+ const char *word, struct strv *output)
+{
+ (void) data;
+
+ // TODO: make it work in other server-related buffers, too, i.e. when we're
+ // completing the third word and the second word is a known channel name
+ if (buffer->type != BUFFER_CHANNEL)
+ return;
+
+ const char *topic = buffer->channel->topic;
+ if (topic && !strncasecmp_ascii (word, topic, strlen (word)))
+ {
+ // We must prepend the channel name if the topic itself starts
+ // with something that could be regarded as a channel name
+ strv_append_owned (output, irc_is_channel (buffer->server, topic)
+ ? xstrdup_printf ("%s %s", buffer->channel->name, topic)
+ : xstrdup (topic));
+ }
+}
+
+static void
+complete_nicknames (struct buffer *buffer, struct completion *data,
+ const char *word, struct strv *output)
+{
+ size_t word_len = strlen (word);
+ if (buffer->type == BUFFER_SERVER)
+ {
+ struct user *self_user = buffer->server->irc_user;
+ if (self_user && !irc_server_strncmp (buffer->server,
+ word, self_user->nickname, word_len))
+ strv_append (output, self_user->nickname);
+ }
+ if (buffer->type != BUFFER_CHANNEL)
+ return;
+
+ LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
+ {
+ const char *nickname = iter->user->nickname;
+ if (irc_server_strncmp (buffer->server, word, nickname, word_len))
+ continue;
+ strv_append_owned (output, data->location == 0
+ ? xstrdup_printf ("%s:", nickname)
+ : xstrdup (nickname));
+ }
+}
+
+static struct strv
+complete_word (struct app_context *ctx, struct buffer *buffer,
+ struct completion *data, const char *word)
+{
+ char *initial = completion_word (data, 0);
+
+ // Start with a placeholder for the longest common prefix
+ struct strv words = strv_make ();
+ strv_append_owned (&words, NULL);
+
+ if (data->location == 0 && *initial == '/')
+ complete_command (ctx, data, word, &words);
+ else if (data->location >= 1 && !strcmp (initial, "/set"))
+ complete_set (ctx, data, word, &words);
+ else if (data->location == 1 && !strcmp (initial, "/help"))
+ {
+ complete_command (ctx, data, word, &words);
+ complete_option (ctx, data, word, &words);
+ }
+ else if (data->location == 1 && !strcmp (initial, "/topic"))
+ {
+ complete_topic (buffer, data, word, &words);
+ complete_nicknames (buffer, data, word, &words);
+ }
+ else
+ complete_nicknames (buffer, data, word, &words);
+
+ cstr_set (&initial, NULL);
+ LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
+ {
+ struct completion_hook *hook = (struct completion_hook *) iter;
+ hook->complete (hook, data, word, &words);
+ }
+
+ if (words.len <= 2)
+ {
+ // When nothing matches, this copies the sentinel value
+ words.vector[0] = words.vector[1];
+ words.vector[1] = NULL;
+ words.len--;
+ }
+ else
+ {
+ size_t prefix = utf8_common_prefix
+ ((const char **) words.vector + 1, words.len - 1);
+ if (!prefix)
+ words.vector[0] = xstrdup (word);
+ else
+ words.vector[0] = xstrndup (words.vector[1], prefix);
+ }
+ return words;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// A special wrapper for iconv_xstrdup() that also fixes indexes into the
+/// original string to point to the right location in the output.
+/// Thanks, Readline! Without you I would have never needed to deal with this.
+static char *
+locale_to_utf8 (struct app_context *ctx, const char *locale,
+ int *indexes[], size_t n_indexes)
+{
+ mbstate_t state;
+ memset (&state, 0, sizeof state);
+
+ size_t remaining = strlen (locale) + 1;
+ const char *p = locale;
+
+ // Reset the shift state, FWIW
+ // TODO: Don't use this iconv handle directly at all, elsewhere in xC.
+ // And ideally use U+FFFD with EILSEQ.
+ (void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL);
+
+ bool fixed[n_indexes];
+ memset (fixed, 0, sizeof fixed);
+
+ struct str utf8 = str_make ();
+ while (true)
+ {
+ size_t len = mbrlen (p, remaining, &state);
+
+ // Incomplete multibyte character or illegal sequence (probably)
+ if (len == (size_t) -2
+ || len == (size_t) -1)
+ {
+ str_free (&utf8);
+ return NULL;
+ }
+
+ // Convert indexes into the multibyte string to UTF-8
+ for (size_t i = 0; i < n_indexes; i++)
+ if (!fixed[i] && *indexes[i] <= p - locale)
+ {
+ *indexes[i] = utf8.len;
+ fixed[i] = true;
+ }
+
+ // End of string
+ if (!len)
+ break;
+
+ // EINVAL (incomplete sequence) should never happen and
+ // EILSEQ neither because we've already checked for that with mbrlen().
+ // E2BIG is what iconv_xstrdup solves. This must succeed.
+ size_t ch_len;
+ char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len);
+ hard_assert (ch != NULL);
+ str_append_data (&utf8, ch, ch_len);
+ free (ch);
+
+ p += len;
+ remaining -= len;
+ }
+ return str_steal (&utf8);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct strv
+make_completions (struct app_context *ctx, struct buffer *buffer,
+ const char *line_utf8, size_t start, size_t end)
+{
+ struct completion comp = completion_make (line_utf8, strlen (line_utf8));
+ completion_locate (&comp, start);
+ char *word = xstrndup (line_utf8 + start, end - start);
+ struct strv completions = complete_word (ctx, buffer, &comp, word);
+ free (word);
+ completion_free (&comp);
+ return completions;
+}
+
+/// Takes a line in locale-specific encoding and position of a word to complete,
+/// returns a vector of matches in locale-specific encoding.
+static char **
+make_input_completions
+ (struct app_context *ctx, const char *line, int start, int end)
+{
+ int *fixes[] = { &start, &end };
+ char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
+ if (!line_utf8)
+ return NULL;
+
+ hard_assert (start >= 0 && end >= 0 && start <= end);
+
+ struct strv completions =
+ make_completions (ctx, ctx->current_buffer, line_utf8, start, end);
+ free (line_utf8);
+ if (!completions.len)
+ {
+ strv_free (&completions);
+ return NULL;
+ }
+ for (size_t i = 0; i < completions.len; i++)
+ {
+ char *converted = iconv_xstrdup
+ (ctx->term_from_utf8, completions.vector[i], -1, NULL);
+ if (!soft_assert (converted))
+ converted = xstrdup ("?");
+ cstr_set (&completions.vector[i], converted);
+ }
+ return completions.vector;
+}
+
+// --- Common code for user actions --------------------------------------------
+
+static void
+toggle_bracketed_paste (bool enable)
+{
+ fprintf (stdout, "\x1b[?2004%c", "lh"[enable]);
+ fflush (stdout);
+}
+
+static void
+suspend_terminal (struct app_context *ctx)
+{
+ // Terminal can get suspended by both the pager and SIGTSTP handling
+ if (ctx->terminal_suspended++ > 0)
+ return;
+
+ toggle_bracketed_paste (false);
+ CALL (ctx->input, hide);
+ poller_fd_reset (&ctx->tty_event);
+
+ CALL_ (ctx->input, prepare, false);
+}
+
+static void
+resume_terminal (struct app_context *ctx)
+{
+ if (--ctx->terminal_suspended > 0)
+ return;
+
+ update_screen_size ();
+ CALL_ (ctx->input, prepare, true);
+ CALL (ctx->input, on_tty_resized);
+
+ toggle_bracketed_paste (true);
+ // In theory we could just print all unseen messages but this is safer
+ buffer_print_backlog (ctx, ctx->current_buffer);
+ // Now it's safe to process any user input
+ poller_fd_set (&ctx->tty_event, POLLIN);
+ CALL (ctx->input, show);
+}
+
+static pid_t
+spawn_helper_child (struct app_context *ctx)
+{
+ suspend_terminal (ctx);
+ pid_t child = fork ();
+ switch (child)
+ {
+ case -1:
+ {
+ int saved_errno = errno;
+ resume_terminal (ctx);
+ errno = saved_errno;
+ break;
+ }
+ case 0:
+ // Put the child in a new foreground process group
+ hard_assert (setpgid (0, 0) != -1);
+ hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
+ break;
+ default:
+ // Make sure of it in the parent as well before continuing
+ (void) setpgid (child, child);
+ }
+ return child;
+}
+
+static void
+redraw_screen (struct app_context *ctx)
+{
+ // If by some circumstance we had the wrong idea
+ CALL (ctx->input, on_tty_resized);
+ update_screen_size ();
+
+ CALL (ctx->input, hide);
+ buffer_print_backlog (ctx, ctx->current_buffer);
+ CALL (ctx->input, show);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+dump_input_to_file (struct app_context *ctx, char *template, struct error **e)
+{
+ mode_t mask = umask (S_IXUSR | S_IRWXG | S_IRWXO);
+ int fd = mkstemp (template);
+ (void) umask (mask);
+
+ if (fd < 0)
+ return error_set (e, "%s", strerror (errno));
+
+ char *input = CALL_ (ctx->input, get_line, NULL);
+ bool success = xwrite (fd, input, strlen (input), e);
+ free (input);
+
+ if (!success)
+ (void) unlink (template);
+
+ xclose (fd);
+ return success;
+}
+
+static char *
+try_dump_input_to_file (struct app_context *ctx)
+{
+ char *template = resolve_filename
+ ("input.XXXXXX", resolve_relative_runtime_template);
+
+ struct error *e = NULL;
+ if (dump_input_to_file (ctx, template, &e))
+ return template;
+
+ log_global_error (ctx, "#s: #s",
+ "Failed to create a temporary file for editing", e->message);
+ error_free (e);
+ free (template);
+ return NULL;
+}
+
+static struct strv
+build_editor_command (struct app_context *ctx, const char *filename)
+{
+ struct strv argv = strv_make ();
+ const char *editor = get_config_string (ctx->config.root, "general.editor");
+ if (!editor)
+ {
+ const char *command;
+ if (!(command = getenv ("VISUAL"))
+ && !(command = getenv ("EDITOR")))
+ command = "vi";
+
+ // Although most visual editors support a "+LINE" argument
+ // (every editor mentioned in the default value of general.editor,
+ // plus vi, mcedit, vis, ...), it isn't particularly useful by itself.
+ // We need to be able to specify the column number.
+ //
+ // Seeing as less popular software may try to process this as a filename
+ // and fail, do not bother with this "undocumented standard feature".
+ strv_append (&argv, command);
+ strv_append (&argv, filename);
+ return argv;
+ }
+
+ int cursor = 0;
+ char *input = CALL_ (ctx->input, get_line, &cursor);
+ hard_assert (cursor >= 0);
+
+ mbstate_t ps;
+ memset (&ps, 0, sizeof ps);
+
+ wchar_t wch;
+ size_t len, processed = 0, line_one_based = 1, column = 0;
+ while (processed < (size_t) cursor
+ && (len = mbrtowc (&wch, input + processed, cursor - processed, &ps))
+ && len != (size_t) -2 && len != (size_t) -1)
+ {
+ // Both VIM and Emacs use the caret notation with columns.
+ // Consciously leaving tabs broken, they're too difficult to handle.
+ int width = wcwidth (wch);
+ if (width < 0)
+ width = 2;
+
+ processed += len;
+ if (wch == '\n')
+ {
+ line_one_based++;
+ column = 0;
+ }
+ else
+ column += width;
+ }
+ free (input);
+
+ // Trivially split the command on spaces and substitute our values
+ struct str argument = str_make ();
+ for (; *editor; editor++)
+ {
+ if (*editor == ' ')
+ {
+ if (argument.len)
+ {
+ strv_append_owned (&argv, str_steal (&argument));
+ argument = str_make ();
+ }
+ continue;
+ }
+ if (*editor != '%' || !editor[1])
+ {
+ str_append_c (&argument, *editor);
+ continue;
+ }
+
+ // None of them are zero-length, thus words don't get lost
+ switch (*++editor)
+ {
+ case 'F':
+ str_append (&argument, filename);
+ continue;
+ case 'L':
+ str_append_printf (&argument, "%zu", line_one_based);
+ continue;
+ case 'C':
+ str_append_printf (&argument, "%zu", column + 1);
+ continue;
+ case 'B':
+ str_append_printf (&argument, "%d", cursor + 1);
+ continue;
+ case '%':
+ case ' ':
+ str_append_c (&argument, *editor);
+ continue;
+ }
+
+ const char *p = editor;
+ if (soft_assert (utf8_decode (&p, strlen (p)) > 0))
+ {
+ log_global_error (ctx, "Unknown substitution variable: %#&s",
+ xstrndup (editor, p - editor));
+ }
+ }
+ if (argument.len)
+ strv_append_owned (&argv, str_steal (&argument));
+ else
+ str_free (&argument);
+ return argv;
+}
+
+static bool
+on_edit_input (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ struct app_context *ctx = user_data;
+
+ char *filename;
+ if (!(filename = try_dump_input_to_file (ctx)))
+ return false;
+
+ struct strv argv = build_editor_command (ctx, filename);
+ if (!argv.len)
+ strv_append (&argv, "true");
+
+ hard_assert (!ctx->running_editor);
+ switch (spawn_helper_child (ctx))
+ {
+ case 0:
+ execvp (argv.vector[0], argv.vector);
+ print_error ("%s: %s",
+ "Failed to launch editor", strerror (errno));
+ _exit (EXIT_FAILURE);
+ case -1:
+ log_global_error (ctx, "#s: #l",
+ "Failed to launch editor", strerror (errno));
+ free (filename);
+ break;
+ default:
+ ctx->running_editor = true;
+ ctx->editor_filename = filename;
+ }
+ strv_free (&argv);
+ return true;
+}
+
+static void
+input_editor_process (struct app_context *ctx)
+{
+ struct str input = str_make ();
+ struct error *e = NULL;
+ if (!read_file (ctx->editor_filename, &input, &e))
+ {
+ log_global_error (ctx, "#s: #s", "Input editing failed", e->message);
+ error_free (e);
+ }
+ else
+ CALL (ctx->input, clear_line);
+
+ if (!CALL_ (ctx->input, insert, input.str))
+ log_global_error (ctx, "#s: #s", "Input editing failed",
+ "could not re-insert the modified text");
+
+ str_free (&input);
+}
+
+static void
+input_editor_cleanup (struct app_context *ctx)
+{
+ if (unlink (ctx->editor_filename))
+ log_global_error (ctx, "Could not unlink `#l': #l",
+ ctx->editor_filename, strerror (errno));
+
+ cstr_set (&ctx->editor_filename, NULL);
+ ctx->running_editor = false;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+launch_pager (struct app_context *ctx,
+ int fd, const char *name, const char *path)
+{
+ hard_assert (!ctx->running_pager);
+ switch (spawn_helper_child (ctx))
+ {
+ case 0:
+ dup2 (fd, STDIN_FILENO);
+ char *localized_name =
+ iconv_xstrdup (ctx->term_from_utf8, (char *) name, -1, NULL);
+ execl ("/bin/sh", "/bin/sh", "-c",
+ get_config_string (ctx->config.root, "general.pager"),
+ PROGRAM_NAME, localized_name, path, NULL);
+ print_error ("%s: %s", "Failed to launch pager", strerror (errno));
+ _exit (EXIT_FAILURE);
+ case -1:
+ log_global_error (ctx, "#s: #l",
+ "Failed to launch pager", strerror (errno));
+ break;
+ default:
+ ctx->running_pager = true;
+ }
+}
+
+static bool
+display_backlog (struct app_context *ctx, int flush_opts)
+{
+ FILE *backlog = tmpfile ();
+ if (!backlog)
+ {
+ log_global_error (ctx, "#s: #l",
+ "Failed to create a temporary file", strerror (errno));
+ return false;
+ }
+
+ if (!get_config_boolean (ctx->config.root,
+ "general.pager_strip_formatting"))
+ flush_opts |= FLUSH_OPT_RAW;
+
+ struct buffer *buffer = ctx->current_buffer;
+ int until_marker =
+ (int) buffer->lines_count - (int) buffer->new_messages_count;
+ for (struct buffer_line *line = buffer->lines; line; line = line->next)
+ {
+ if (until_marker-- == 0
+ && buffer->new_messages_count != buffer->lines_count)
+ buffer_print_read_marker (ctx, backlog, flush_opts);
+ if (buffer_line_will_show_up (buffer, line))
+ buffer_line_write_to_backlog (ctx, line, backlog, flush_opts);
+ }
+
+ // So that it is obvious if the last line in the buffer is not from today
+ buffer_update_time (ctx, time (NULL), backlog, flush_opts);
+
+ rewind (backlog);
+ set_cloexec (fileno (backlog));
+ launch_pager (ctx, fileno (backlog), buffer->name, NULL);
+ fclose (backlog);
+ return true;
+}
+
+static bool
+on_display_backlog (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ return display_backlog (user_data, 0);
+}
+
+static bool
+on_display_backlog_nowrap (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ return display_backlog (user_data, FLUSH_OPT_NOWRAP);
+}
+
+static FILE *
+open_log_path (struct app_context *ctx, struct buffer *buffer, const char *path)
+{
+ FILE *fp = fopen (path, "rb");
+ if (!fp)
+ {
+ log_global_error (ctx,
+ "Failed to open `#l': #l", path, strerror (errno));
+ return NULL;
+ }
+
+ if (buffer->log_file)
+ // The regular flush will log any error eventually
+ (void) fflush (buffer->log_file);
+
+ set_cloexec (fileno (fp));
+ return fp;
+}
+
+static bool
+on_display_full_log (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ struct app_context *ctx = user_data;
+
+ struct buffer *buffer = ctx->current_buffer;
+ char *path = buffer_get_log_path (buffer);
+ FILE *full_log = open_log_path (ctx, buffer, path);
+ if (!full_log)
+ {
+ free (path);
+ return false;
+ }
+
+ launch_pager (ctx, fileno (full_log), buffer->name, path);
+ fclose (full_log);
+ free (path);
+ return true;
+}
+
+static bool
+on_toggle_unimportant (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ struct app_context *ctx = user_data;
+ buffer_toggle_unimportant (ctx, ctx->current_buffer);
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+on_goto_buffer (int count, int key, void *user_data)
+{
+ (void) count;
+ struct app_context *ctx = user_data;
+
+ int n = key - '0';
+ if (n < 0 || n > 9)
+ return false;
+
+ // There's no buffer zero
+ if (n == 0)
+ n = 10;
+
+ if (!ctx->last_buffer || buffer_get_index (ctx, ctx->current_buffer) != n)
+ return buffer_goto (ctx, n);
+
+ // Fast switching between two buffers
+ buffer_activate (ctx, ctx->last_buffer);
+ return true;
+}
+
+static bool
+on_previous_buffer (int count, int key, void *user_data)
+{
+ (void) key;
+ buffer_activate (user_data, buffer_previous (user_data, count));
+ return true;
+}
+
+static bool
+on_next_buffer (int count, int key, void *user_data)
+{
+ (void) key;
+ buffer_activate (user_data, buffer_next (user_data, count));
+ return true;
+}
+
+static bool
+on_switch_buffer (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+ struct app_context *ctx = user_data;
+
+ if (!ctx->last_buffer)
+ return false;
+ buffer_activate (ctx, ctx->last_buffer);
+ return true;
+}
+
+static bool
+on_goto_highlight (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+
+ struct app_context *ctx = user_data;
+ struct buffer *iter = ctx->current_buffer;;
+ do
+ {
+ if (!(iter = iter->next))
+ iter = ctx->buffers;
+ if (iter == ctx->current_buffer)
+ return false;
+ }
+ while (!iter->highlighted);
+ buffer_activate (ctx, iter);
+ return true;
+}
+
+static bool
+on_goto_activity (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+
+ struct app_context *ctx = user_data;
+ struct buffer *iter = ctx->current_buffer;
+ do
+ {
+ if (!(iter = iter->next))
+ iter = ctx->buffers;
+ if (iter == ctx->current_buffer)
+ return false;
+ }
+ while (iter->new_messages_count == iter->new_unimportant_count);
+ buffer_activate (ctx, iter);
+ return true;
+}
+
+static bool
+on_move_buffer_left (int count, int key, void *user_data)
+{
+ (void) key;
+
+ struct app_context *ctx = user_data;
+ int total = buffer_count (ctx);
+ int n = buffer_get_index (ctx, ctx->current_buffer) - count;
+ buffer_move (ctx, ctx->current_buffer, n <= 0
+ ? (total + n % total)
+ : ((n - 1) % total + 1));
+ return true;
+}
+
+static bool
+on_move_buffer_right (int count, int key, void *user_data)
+{
+ return on_move_buffer_left (-count, key, user_data);
+}
+
+static bool
+on_redraw_screen (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+
+ redraw_screen (user_data);
+ return true;
+}
+
+static bool
+on_insert_attribute (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+
+ struct app_context *ctx = user_data;
+ ctx->awaiting_formatting_escape = true;
+ return true;
+}
+
+static bool
+on_start_paste_mode (int count, int key, void *user_data)
+{
+ (void) count;
+ (void) key;
+
+ struct app_context *ctx = user_data;
+ ctx->in_bracketed_paste = true;
+ return true;
+}
+
+static void
+input_add_functions (void *user_data)
+{
+ struct app_context *ctx = user_data;
+#define XX(...) CALL_ (ctx->input, register_fn, __VA_ARGS__, ctx);
+ XX ("previous-buffer", "Previous buffer", on_previous_buffer)
+ XX ("next-buffer", "Next buffer", on_next_buffer)
+ XX ("goto-buffer", "Go to buffer", on_goto_buffer)
+ XX ("switch-buffer", "Switch buffer", on_switch_buffer)
+ XX ("goto-highlight", "Go to highlight", on_goto_highlight)
+ XX ("goto-activity", "Go to activity", on_goto_activity)
+ XX ("move-buffer-left", "Move buffer left", on_move_buffer_left)
+ XX ("move-buffer-right", "Move buffer right", on_move_buffer_right)
+ XX ("display-backlog", "Show backlog", on_display_backlog)
+ XX ("display-backlog-nw", "Non-wrapped log", on_display_backlog_nowrap)
+ XX ("display-full-log", "Show full log", on_display_full_log)
+ XX ("toggle-unimportant", "Toggle junk msgs", on_toggle_unimportant)
+ XX ("edit-input", "Edit input", on_edit_input)
+ XX ("redraw-screen", "Redraw screen", on_redraw_screen)
+ XX ("insert-attribute", "IRC formatting", on_insert_attribute)
+ XX ("start-paste-mode", "Bracketed paste", on_start_paste_mode)
+#undef XX
+}
+
+static void
+bind_common_keys (struct app_context *ctx)
+{
+ struct input *self = ctx->input;
+ CALL_ (self, bind_control, 'p', "previous-buffer");
+ CALL_ (self, bind_control, 'n', "next-buffer");
+
+ // Redefine M-0 through M-9 to switch buffers
+ for (int i = 0; i <= 9; i++)
+ CALL_ (self, bind_meta, '0' + i, "goto-buffer");
+
+ CALL_ (self, bind_meta, '\t', "switch-buffer");
+ CALL_ (self, bind_meta, '!', "goto-highlight");
+ CALL_ (self, bind_meta, 'a', "goto-activity");
+ CALL_ (self, bind_meta, 'm', "insert-attribute");
+ CALL_ (self, bind_meta, 'h', "display-full-log");
+ CALL_ (self, bind_meta, 'H', "toggle-unimportant");
+ CALL_ (self, bind_meta, 'e', "edit-input");
+
+ if (key_f5) CALL_ (self, bind, key_f5, "previous-buffer");
+ if (key_f6) CALL_ (self, bind, key_f6, "next-buffer");
+ if (key_ppage) CALL_ (self, bind, key_ppage, "display-backlog");
+
+ if (clear_screen)
+ CALL_ (self, bind_control, 'l', "redraw-screen");
+
+ CALL_ (self, bind, "\x1b[200~", "start-paste-mode");
+}
+
+// --- GNU Readline user actions -----------------------------------------------
+
+#ifdef HAVE_READLINE
+
+static int
+on_readline_return (int count, int key)
+{
+ (void) count;
+ (void) key;
+
+ // Let readline pass the line to our input handler
+ rl_done = 1;
+
+ struct app_context *ctx = g_ctx;
+ struct input_rl *self = (struct input_rl *) ctx->input;
+
+ // Hide the line, don't redisplay it
+ CALL (ctx->input, hide);
+ input_rl__restore (self);
+ return 0;
+}
+
+static void
+on_readline_input (char *line)
+{
+ struct app_context *ctx = g_ctx;
+ struct input_rl *self = (struct input_rl *) ctx->input;
+
+ if (line)
+ {
+ if (*line)
+ add_history (line);
+
+ // readline always erases the input line after returning from here,
+ // but we don't want that to happen if the command to be executed
+ // would switch the buffer (we'd keep the already executed command in
+ // the old buffer and delete any input restored from the new buffer)
+ strv_append_owned (&ctx->pending_input, line);
+ poller_idle_set (&ctx->input_event);
+ }
+ else
+ {
+ // Prevent readline from showing the prompt twice for w/e reason
+ CALL (ctx->input, hide);
+ input_rl__restore (self);
+
+ CALL (ctx->input, ding);
+ }
+
+ if (self->active)
+ // Readline automatically redisplays it
+ self->prompt_shown = 1;
+}
+
+static char **
+app_readline_completion (const char *text, int start, int end)
+{
+ // We will reconstruct that ourselves
+ (void) text;
+
+ // Don't iterate over filenames and stuff
+ rl_attempted_completion_over = true;
+
+ return make_input_completions (g_ctx, rl_line_buffer, start, end);
+}
+
+static void
+app_readline_display_matches (char **matches, int len, int longest)
+{
+ struct app_context *ctx = g_ctx;
+ CALL (ctx->input, hide);
+ rl_display_match_list (matches, len, longest);
+ CALL (ctx->input, show);
+}
+
+static int
+app_readline_init (void)
+{
+ struct app_context *ctx = g_ctx;
+ struct input *self = ctx->input;
+
+ // XXX: maybe use rl_make_bare_keymap() and start from there;
+ // our dear user could potentionally rig things up in a way that might
+ // result in some funny unspecified behaviour
+
+ // For vi mode, enabling "show-mode-in-prompt" is recommended as there is
+ // no easy way to indicate mode changes otherwise.
+
+ rl_add_defun ("send-line", on_readline_return, -1);
+ bind_common_keys (ctx);
+
+ // Move native history commands
+ CALL_ (self, bind_meta, 'p', "previous-history");
+ CALL_ (self, bind_meta, 'n', "next-history");
+
+ // We need to hide the prompt and input first
+ rl_bind_key (RETURN, rl_named_function ("send-line"));
+ CALL_ (self, bind_control, 'j', "send-line");
+
+ rl_completion_display_matches_hook = app_readline_display_matches;
+
+ rl_variable_bind ("completion-ignore-case", "on");
+ rl_variable_bind ("menu-complete-display-prefix", "on");
+ rl_bind_key (TAB, rl_named_function ("menu-complete"));
+ if (key_btab)
+ CALL_ (self, bind, key_btab, "menu-complete-backward");
+ return 0;
+}
+
+#endif // HAVE_READLINE
+
+// --- BSD Editline user actions -----------------------------------------------
+
+#ifdef HAVE_EDITLINE
+
+static unsigned char
+on_editline_complete (EditLine *editline, int key)
+{
+ (void) key;
+ struct app_context *ctx = g_ctx;
+
+ // First prepare what Readline would have normally done for us...
+ const LineInfo *info_mb = el_line (editline);
+ int len = info_mb->lastchar - info_mb->buffer;
+ int point = info_mb->cursor - info_mb->buffer;
+ char *copy = xstrndup (info_mb->buffer, len);
+
+ // XXX: possibly incorrect wrt. shift state encodings
+ int el_start = point, el_end = point;
+ while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
+ el_start--;
+
+ char **completions = make_input_completions (ctx, copy, el_start, el_end);
+
+ // XXX: possibly incorrect wrt. shift state encodings
+ copy[el_end] = '\0';
+ int el_len = mbstowcs (NULL, copy + el_start, 0);
+ free (copy);
+
+ if (!completions)
+ return CC_REFRESH_BEEP;
+
+ // Remove the original word
+ el_wdeletestr (editline, el_len);
+
+ // Insert the best match instead
+ el_insertstr (editline, completions[0]);
+
+ // I'm not sure if Readline's menu-complete can at all be implemented
+ // with Editline--we have no way of detecting what the last executed handler
+ // was. Employ the formatter's wrapping feature to spew all options.
+ bool only_match = !completions[1];
+ if (!only_match)
+ {
+ CALL (ctx->input, hide);
+ redraw_screen (ctx);
+
+ struct formatter f = formatter_make (ctx, NULL);
+ for (char **p = completions; *++p; )
+ formatter_add (&f, " #l", *p);
+ formatter_add (&f, "\n");
+ formatter_flush (&f, stdout, 0);
+ formatter_free (&f);
+
+ CALL (ctx->input, show);
+ }
+
+ for (char **p = completions; *p; p++)
+ free (*p);
+ free (completions);
+ if (!only_match)
+ return CC_REFRESH_BEEP;
+
+ // If there actually is just one match, finish the word
+ el_insertstr (editline, " ");
+ return CC_REFRESH;
+}
+
+static unsigned char
+on_editline_return (EditLine *editline, int key)
+{
+ (void) key;
+ struct app_context *ctx = g_ctx;
+ struct input_el *self = (struct input_el *) ctx->input;
+
+ const LineInfoW *info = el_wline (editline);
+ int len = info->lastchar - info->buffer;
+ wchar_t *line = calloc (sizeof *info->buffer, len + 1);
+ memcpy (line, info->buffer, sizeof *info->buffer * len);
+
+ if (*line)
+ {
+ HistEventW ev;
+ history_w (self->current->history, &ev, H_ENTER, line);
+ }
+ free (line);
+
+ // on_pending_input() expects a multibyte string
+ const LineInfo *info_mb = el_line (editline);
+ strv_append_owned (&ctx->pending_input,
+ xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
+ poller_idle_set (&ctx->input_event);
+
+ // We must invoke ch_reset(), which isn't done for us with EL_UNBUFFERED.
+ input_el__start_over (self);
+ return CC_REFRESH;
+}
+
+static void
+app_editline_init (struct input_el *self)
+{
+ // el_set() leaks memory in 20150325 and other versions, we need wchar_t
+ el_wset (self->editline, EL_ADDFN,
+ L"send-line", L"Send line", on_editline_return);
+ el_wset (self->editline, EL_ADDFN,
+ L"complete", L"Complete word", on_editline_complete);
+
+ struct input *input = &self->super;
+ input->add_functions (input->user_data);
+ bind_common_keys (g_ctx);
+
+ // Move native history commands
+ CALL_ (input, bind_meta, 'p', "ed-prev-history");
+ CALL_ (input, bind_meta, 'n', "ed-next-history");
+
+ // No, editline, it's not supposed to kill the entire line
+ CALL_ (input, bind_control, 'w', "ed-delete-prev-word");
+ // Just what are you doing?
+ CALL_ (input, bind_control, 'u', "vi-kill-line-prev");
+
+ // We need to hide the prompt and input first
+ CALL_ (input, bind, "\r", "send-line");
+ CALL_ (input, bind, "\n", "send-line");
+
+ CALL_ (input, bind_control, 'i', "complete");
+
+ // Source the user's defaults file
+ el_source (self->editline, NULL);
+
+ // See input_el__redisplay(), functionally important
+ CALL_ (input, bind_control, 'q', "ed-redisplay");
+ // This is what buffered el_wgets() does, functionally important
+ CALL_ (input, bind_control, 'c', "ed-start-over");
+}
+
+#endif // HAVE_EDITLINE
+
+// --- Configuration loading ---------------------------------------------------
+
+static const char *g_first_time_help[] =
+{
+ "",
+ "\x02Welcome to xC!",
+ "",
+ "To get a list of all commands, type \x02/help\x02. To obtain",
+ "more information on a command or option, simply add it as",
+ "a parameter, e.g. \x02/help set\x02 or \x02/help general.logging\x02.",
+ "",
+ "To switch between buffers, press \x02"
+ "F5/Ctrl-P\x02 or \x02" "F6/Ctrl-N\x02.",
+ "",
+ "Finally, adding a network is as simple as:",
+ " - \x02/server add IRCnet\x02",
+ " - \x02/set servers.IRCnet.addresses = \"open.ircnet.net\"\x02",
+ " - \x02/connect IRCnet\x02",
+ "",
+ "That should be enough to get you started. Have fun!",
+ ""
+};
+
+static void
+show_first_time_help (struct app_context *ctx)
+{
+ for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++)
+ log_global_indent (ctx, "#m", g_first_time_help[i]);
+}
+
+const char *g_default_aliases[][2] =
+{
+ { "c", "/buffer clear" }, { "close", "/buffer close" },
+ { "j", "/join $*" }, { "p", "/part $*" },
+ { "k", "/kick $*" }, { "kb", "/kickban $*" },
+ { "m", "/msg $*" }, { "q", "/query $*" },
+ { "n", "/names $*" }, { "t", "/topic $*" },
+ { "w", "/who $*" }, { "wi", "/whois $*" },
+ { "ww", "/whowas $*" },
+};
+
+static void
+load_default_aliases (struct app_context *ctx)
+{
+ struct str_map *aliases = get_aliases_config (ctx);
+ for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
+ {
+ const char **pair = g_default_aliases[i];
+ str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1]));
+ }
+}
+
+static void
+load_configuration (struct app_context *ctx)
+{
+ // In theory, we could ensure that only one instance is running by locking
+ // the configuration file and ensuring here that it exists. This is
+ // however brittle, as it may be unlinked without the application noticing.
+
+ struct config_item *root = NULL;
+ struct error *e = NULL;
+
+ char *filename = resolve_filename
+ (PROGRAM_NAME ".conf", resolve_relative_config_filename);
+ if (filename)
+ root = config_read_from_file (filename, &e);
+ else
+ log_global_error (ctx, "Configuration file not found");
+ free (filename);
+
+ if (e)
+ {
+ log_global_error (ctx, "Cannot load configuration: #s", e->message);
+ log_global_error (ctx,
+ "Please either fix the configuration file or remove it");
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ if (root)
+ {
+ config_load (&ctx->config, root);
+ log_global_status (ctx, "Configuration loaded");
+ }
+ else
+ {
+ show_first_time_help (ctx);
+ load_default_aliases (ctx);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+load_servers (struct app_context *ctx)
+{
+ struct str_map_iter iter = str_map_iter_make (get_servers_config (ctx));
+
+ struct config_item *subtree;
+ while ((subtree = str_map_iter_next (&iter)))
+ {
+ const char *name = iter.link->key;
+ const char *err;
+ if (subtree->type != CONFIG_ITEM_OBJECT)
+ log_global_error (ctx, "Error in configuration: "
+ "ignoring server `#s' as it's not an object", name);
+ else if ((err = check_server_name_for_addition (ctx, name)))
+ log_global_error (ctx, "Cannot load server `#s': #s", name, err);
+ else
+ server_add (ctx, name, subtree);
+ }
+}
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+/// Program termination has been requested by a signal
+static volatile sig_atomic_t g_termination_requested;
+/// The window has changed in size
+static volatile sig_atomic_t g_winch_received;
+
+static void
+postpone_signal_handling (char id)
+{
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], &id, 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+signal_superhandler (int signum)
+{
+ switch (signum)
+ {
+ case SIGWINCH:
+ g_winch_received = true;
+ postpone_signal_handling ('w');
+ break;
+ case SIGINT:
+ case SIGTERM:
+ g_termination_requested = true;
+ postpone_signal_handling ('t');
+ break;
+ case SIGCHLD:
+ postpone_signal_handling ('c');
+ break;
+ case SIGTSTP:
+ postpone_signal_handling ('s');
+ break;
+ default:
+ hard_assert (!"unhandled signal");
+ }
+}
+
+static void
+setup_signal_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ signal (SIGPIPE, SIG_IGN);
+
+ // So that we can write to the terminal while we're running a pager.
+ // This is also inherited by the child so that it doesn't stop
+ // when it calls tcsetpgrp().
+ signal (SIGTTOU, SIG_IGN);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = signal_superhandler;
+ sigemptyset (&sa.sa_mask);
+
+ if (sigaction (SIGWINCH, &sa, NULL) == -1
+ || sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1
+ || sigaction (SIGTSTP, &sa, NULL) == -1
+ || sigaction (SIGCHLD, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+}
+
+// --- I/O event handlers ------------------------------------------------------
+
+static bool
+try_reap_child (struct app_context *ctx)
+{
+ int status;
+ pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED);
+
+ if (zombie == -1)
+ {
+ if (errno == ECHILD) return false;
+ if (errno == EINTR) return true;
+ exit_fatal ("%s: %s", "waitpid", strerror (errno));
+ }
+ if (!zombie)
+ return false;
+
+ if (WIFSTOPPED (status))
+ {
+ // We could also send SIGCONT but what's the point
+ log_global_debug (ctx,
+ "A child has been stopped, killing its process group");
+ kill (-zombie, SIGKILL);
+ return true;
+ }
+
+ if (ctx->running_pager)
+ ctx->running_pager = false;
+ else if (!ctx->running_editor)
+ {
+ log_global_debug (ctx, "An unknown child has died");
+ return true;
+ }
+
+ hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
+ resume_terminal (ctx);
+
+ if (WIFSIGNALED (status))
+ log_global_error (ctx,
+ "Child died from signal #d", WTERMSIG (status));
+ else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
+ log_global_error (ctx,
+ "Child returned status #d", WEXITSTATUS (status));
+ else if (ctx->running_editor)
+ input_editor_process (ctx);
+
+ if (ctx->running_editor)
+ input_editor_cleanup (ctx);
+ return true;
+}
+
+static void
+on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
+{
+ char id = 0;
+ (void) read (fd->fd, &id, 1);
+
+ // Stop ourselves cleanly, even if it makes little sense to do this
+ if (id == 's')
+ {
+ suspend_terminal (ctx);
+ kill (getpid (), SIGSTOP);
+ g_winch_received = true;
+ resume_terminal (ctx);
+ }
+
+ // Reap all dead children (since the signal pipe may overflow etc. we run
+ // waitpid() in a loop to return all the zombies it knows about).
+ while (try_reap_child (ctx))
+ ;
+
+ if (g_termination_requested)
+ {
+ g_termination_requested = false;
+ request_quit (ctx, NULL);
+ }
+ if (g_winch_received)
+ {
+ g_winch_received = false;
+ redraw_screen (ctx);
+ }
+}
+
+static void
+process_formatting_escape (const struct pollfd *fd, struct app_context *ctx)
+{
+ // There's no other way with libedit, as both el_getc() in a function
+ // handler and CC_ARGHACK would block execution
+ struct str *buf = &ctx->input_buffer;
+ str_reserve (buf, 1);
+ if (read (fd->fd, buf->str + buf->len, 1) != 1)
+ goto error;
+ buf->str[++buf->len] = '\0';
+
+ // XXX: I think this should be global and shared with Readline/libedit
+ mbstate_t state;
+ memset (&state, 0, sizeof state);
+
+ size_t len = mbrlen (buf->str, buf->len, &state);
+
+ // Illegal sequence
+ if (len == (size_t) -1)
+ goto error;
+
+ // Incomplete multibyte character
+ if (len == (size_t) -2)
+ return;
+
+ if (buf->len != 1)
+ goto error;
+
+ // Letters mostly taken from their caret escapes + HTML element names.
+ // Additionally, 'm' stands for mono, 'x' for cross, 'r' for reset.
+ switch (buf->str[0])
+ {
+ case 'b' ^ 96:
+ case 'b': CALL_ (ctx->input, insert, "\x02"); break;
+ case 'c': CALL_ (ctx->input, insert, "\x03"); break;
+ case 'q':
+ case 'm': CALL_ (ctx->input, insert, "\x11"); break;
+ case 'v': CALL_ (ctx->input, insert, "\x16"); break;
+ case 'i' ^ 96:
+ case 'i':
+ case ']': CALL_ (ctx->input, insert, "\x1d"); break;
+ case 's' ^ 96:
+ case 's':
+ case 'x' ^ 96:
+ case 'x':
+ case '^': CALL_ (ctx->input, insert, "\x1e"); break;
+ case 'u' ^ 96:
+ case 'u':
+ case '_': CALL_ (ctx->input, insert, "\x1f"); break;
+ case 'r':
+ case 'o': CALL_ (ctx->input, insert, "\x0f"); break;
+
+ default:
+ goto error;
+ }
+ goto done;
+
+error:
+ CALL (ctx->input, ding);
+done:
+ str_reset (buf);
+ ctx->awaiting_formatting_escape = false;
+}
+
+#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted
+
+static bool
+insert_paste (struct app_context *ctx, char *paste, size_t len)
+{
+ if (!get_config_boolean (ctx->config.root, "general.process_pasted_text"))
+ return CALL_ (ctx->input, insert, paste);
+
+ // Without ICRNL, which Editline keeps but Readline doesn't,
+ // the terminal sends newlines as carriage returns (seen on urxvt)
+ for (size_t i = 0; i < len; i++)
+ if (paste[i] == '\r')
+ paste[i] = '\n';
+
+ int position = 0;
+ char *input = CALL_ (ctx->input, get_line, &position);
+ bool quote_first_slash = !position || strchr ("\r\n", input[position - 1]);
+ free (input);
+
+ // Executing commands by accident is much more common than pasting them
+ // intentionally, although the latter may also have security consequences
+ struct str processed = str_make ();
+ str_reserve (&processed, len);
+ for (size_t i = 0; i < len; i++)
+ {
+ if (paste[i] == '/'
+ && ((!i && quote_first_slash) || (i && paste[i - 1] == '\n')))
+ str_append_c (&processed, paste[i]);
+ str_append_c (&processed, paste[i]);
+ }
+
+ bool success = CALL_ (ctx->input, insert, processed.str);
+ str_free (&processed);
+ return success;
+}
+
+static void
+process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
+{
+ struct str *buf = &ctx->input_buffer;
+ str_reserve (buf, 1);
+ if (read (fd->fd, buf->str + buf->len, 1) != 1)
+ goto error;
+ buf->str[++buf->len] = '\0';
+
+ static const char stop_mark[] = "\x1b[201~";
+ static const size_t stop_mark_len = sizeof stop_mark - 1;
+ if (buf->len < stop_mark_len)
+ return;
+
+ size_t text_len = buf->len - stop_mark_len;
+ if (memcmp (buf->str + text_len, stop_mark, stop_mark_len))
+ return;
+
+ // Avoid endless flooding of the buffer
+ if (text_len > BRACKETED_PASTE_LIMIT)
+ log_global_error (ctx, "Paste trimmed to #d bytes",
+ (int) (text_len = BRACKETED_PASTE_LIMIT));
+
+ buf->str[text_len] = '\0';
+ if (insert_paste (ctx, buf->str, text_len))
+ goto done;
+
+error:
+ CALL (ctx->input, ding);
+ log_global_error (ctx, "Paste failed");
+done:
+ str_reset (buf);
+ ctx->in_bracketed_paste = false;
+}
+
+static void
+reset_autoaway (struct app_context *ctx)
+{
+ // Stop the last one if it's been disabled altogether in the meantime
+ poller_timer_reset (&ctx->autoaway_tmr);
+
+ // Unset any automated statuses that are active right at this moment
+ struct str_map_iter iter = str_map_iter_make (&ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ {
+ if (s->autoaway_active
+ && s->irc_user
+ && s->irc_user->away)
+ irc_send (s, "AWAY");
+
+ s->autoaway_active = false;
+ }
+
+ // And potentially start a new auto-away timer
+ int64_t delay = get_config_integer
+ (ctx->config.root, "general.autoaway_delay");
+ if (delay)
+ poller_timer_set (&ctx->autoaway_tmr, delay * 1000);
+}
+
+static void
+on_autoaway_timer (struct app_context *ctx)
+{
+ // An empty message would unset any away status, so let's ignore that
+ const char *message = get_config_string
+ (ctx->config.root, "general.autoaway_message");
+ if (!message || !*message)
+ return;
+
+ struct str_map_iter iter = str_map_iter_make (&ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ {
+ // If the user has already been marked as away,
+ // don't override his current away status
+ if (s->irc_user
+ && s->irc_user->away)
+ continue;
+
+ irc_send (s, "AWAY :%s", message);
+ s->autoaway_active = true;
+ }
+}
+
+static void
+on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
+{
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ if (ctx->awaiting_formatting_escape)
+ process_formatting_escape (fd, ctx);
+ else if (ctx->in_bracketed_paste)
+ process_bracketed_paste (fd, ctx);
+ else if (!ctx->quitting)
+ CALL (ctx->input, on_tty_readable);
+
+ // User activity detected, stop current auto-away and start anew;
+ // since they might have just changed the settings, do this last
+ reset_autoaway (ctx);
+}
+
+static void
+rearm_flush_timer (struct app_context *ctx)
+{
+ poller_timer_set (&ctx->flush_timer, 60 * 1000);
+}
+
+static void
+on_flush_timer (struct app_context *ctx)
+{
+ // I guess we don't need to do anything more complicated
+ fflush (NULL);
+
+ // It would be a bit problematic to handle it properly, so do this at least
+ LIST_FOR_EACH (struct buffer, buffer, ctx->buffers)
+ {
+ if (!buffer->log_file || !ferror (buffer->log_file))
+ continue;
+
+ // Might be a transient error such as running out of disk space,
+ // keep notifying of the problem until it disappears
+ clearerr (buffer->log_file);
+ log_global (ctx, BUFFER_LINE_SKIP_FILE, BUFFER_LINE_ERROR,
+ "Log write failure detected for #s", buffer->name);
+ }
+
+#ifdef LOMEM
+ // Lua should normally be reasonable and collect garbage when needed,
+ // though we can try to push it. This is a reasonable place.
+ LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
+ if (iter->vtable->gc)
+ iter->vtable->gc (iter);
+#endif // LOMEM
+
+ rearm_flush_timer (ctx);
+}
+
+static void
+rearm_date_change_timer (struct app_context *ctx)
+{
+ struct tm tm_;
+ const time_t now = time (NULL);
+ if (!soft_assert (localtime_r (&now, &tm_)))
+ return;
+
+ tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0;
+ tm_.tm_mday++;
+ tm_.tm_isdst = -1;
+
+ const time_t midnight = mktime (&tm_);
+ if (!soft_assert (midnight != (time_t) -1))
+ return;
+ poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000);
+}
+
+static void
+on_date_change_timer (struct app_context *ctx)
+{
+ if (ctx->terminal_suspended <= 0)
+ {
+ CALL (ctx->input, hide);
+ buffer_update_time (ctx, time (NULL), stdout, 0);
+ CALL (ctx->input, show);
+ }
+ rearm_date_change_timer (ctx);
+}
+
+static void
+on_pending_input (struct app_context *ctx)
+{
+ poller_idle_reset (&ctx->input_event);
+ for (size_t i = 0; i < ctx->pending_input.len; i++)
+ {
+ char *input = iconv_xstrdup
+ (ctx->term_to_utf8, ctx->pending_input.vector[i], -1, NULL);
+ if (input)
+ process_input (ctx, ctx->current_buffer, input);
+ else
+ print_error ("character conversion failed for: %s", "user input");
+ free (input);
+ }
+ strv_reset (&ctx->pending_input);
+}
+
+static void
+init_poller_events (struct app_context *ctx)
+{
+ ctx->signal_event = poller_fd_make (&ctx->poller, g_signal_pipe[0]);
+ ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
+ ctx->signal_event.user_data = ctx;
+ poller_fd_set (&ctx->signal_event, POLLIN);
+
+ ctx->tty_event = poller_fd_make (&ctx->poller, STDIN_FILENO);
+ ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
+ ctx->tty_event.user_data = ctx;
+ poller_fd_set (&ctx->tty_event, POLLIN);
+
+ ctx->flush_timer = poller_timer_make (&ctx->poller);
+ ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
+ ctx->flush_timer.user_data = ctx;
+ rearm_flush_timer (ctx);
+
+ ctx->date_chg_tmr = poller_timer_make (&ctx->poller);
+ ctx->date_chg_tmr.dispatcher = (poller_timer_fn) on_date_change_timer;
+ ctx->date_chg_tmr.user_data = ctx;
+ rearm_date_change_timer (ctx);
+
+ ctx->autoaway_tmr = poller_timer_make (&ctx->poller);
+ ctx->autoaway_tmr.dispatcher = (poller_timer_fn) on_autoaway_timer;
+ ctx->autoaway_tmr.user_data = ctx;
+
+ ctx->prompt_event = poller_idle_make (&ctx->poller);
+ ctx->prompt_event.dispatcher = (poller_idle_fn) on_refresh_prompt;
+ ctx->prompt_event.user_data = ctx;
+
+ ctx->input_event = poller_idle_make (&ctx->poller);
+ ctx->input_event.dispatcher = (poller_idle_fn) on_pending_input;
+ ctx->input_event.user_data = ctx;
+}
+
+// --- Relay processing --------------------------------------------------------
+
+// XXX: This could be below completion code if reset_autoaway() was higher up.
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+client_resync (struct client *c)
+{
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->servers);
+ struct server *s;
+ while ((s = str_map_iter_next (&iter)))
+ {
+ relay_prepare_server_update (c->ctx, s);
+ relay_send (c);
+ }
+
+ LIST_FOR_EACH (struct buffer, buffer, c->ctx->buffers)
+ {
+ relay_prepare_buffer_update (c->ctx, buffer);
+ relay_send (c);
+ relay_prepare_buffer_stats (c->ctx, buffer);
+ relay_send (c);
+
+ LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
+ {
+ relay_prepare_buffer_line (c->ctx, buffer, line, false);
+ relay_send (c);
+ }
+ }
+
+ relay_prepare_buffer_activate (c->ctx, c->ctx->current_buffer);
+ relay_send (c);
+}
+
+static const char *
+client_message_buffer_name (const struct relay_command_message *m)
+{
+ switch (m->data.command)
+ {
+ case RELAY_COMMAND_BUFFER_COMPLETE:
+ return m->data.buffer_input.buffer_name.str;
+ case RELAY_COMMAND_BUFFER_ACTIVATE:
+ return m->data.buffer_activate.buffer_name.str;
+ case RELAY_COMMAND_BUFFER_INPUT:
+ return m->data.buffer_input.buffer_name.str;
+ case RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT:
+ return m->data.buffer_toggle_unimportant.buffer_name.str;
+ case RELAY_COMMAND_BUFFER_LOG:
+ return m->data.buffer_log.buffer_name.str;
+ default:
+ return NULL;
+ }
+}
+
+static void
+client_process_buffer_complete (struct client *c, uint32_t seq,
+ struct buffer *buffer, struct relay_command_data_buffer_complete *req)
+{
+ struct str *line = &req->text;
+ uint32_t end = req->position;
+ if (line->len < end || line->len != strlen (line->str))
+ {
+ relay_prepare_error (c->ctx, seq, "Invalid arguments");
+ goto out;
+ }
+
+ uint32_t start = end;
+ while (start && !strchr (WORD_BREAKING_CHARS, line->str[start - 1]))
+ start--;
+
+ struct strv completions =
+ make_completions (c->ctx, buffer, line->str, start, end);
+ if (completions.len > UINT32_MAX)
+ {
+ relay_prepare_error (c->ctx, seq, "Internal error");
+ goto out_internal;
+ }
+
+ struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
+ e->data.command = RELAY_COMMAND_BUFFER_COMPLETE;
+ struct relay_response_data_buffer_complete *resp =
+ &e->data.buffer_complete;
+ resp->start = start;
+ resp->completions_len = completions.len;
+ resp->completions = xcalloc (completions.len, sizeof *resp->completions);
+ for (size_t i = 0; i < completions.len; i++)
+ resp->completions[i] = str_from_cstr (completions.vector[i]);
+
+out_internal:
+ strv_free (&completions);
+out:
+ relay_send (c);
+}
+
+static void
+client_process_buffer_log
+ (struct client *c, uint32_t seq, struct buffer *buffer)
+{
+ struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
+ e->data.command = RELAY_COMMAND_BUFFER_LOG;
+
+ char *path = buffer_get_log_path (buffer);
+ FILE *fp = open_log_path (c->ctx, buffer, path);
+ if (fp)
+ {
+ struct str log = str_make ();
+ char buf[BUFSIZ];
+ size_t len;
+ while ((len = fread (buf, 1, sizeof buf, fp)))
+ str_append_data (&log, buf, len);
+ if (ferror (fp))
+ log_global_error (c->ctx, "Failed to read `#l': #l",
+ path, strerror (errno));
+
+ // On overflow, it will later fail serialization.
+ e->data.buffer_log.log_len = MIN (UINT32_MAX, log.len);
+ e->data.buffer_log.log = (uint8_t *) str_steal (&log);
+ fclose (fp);
+ }
+
+ // XXX: We log failures to the global buffer,
+ // so the client just receives nothing if there is no log file.
+
+ free (path);
+ relay_send (c);
+}
+
+static bool
+client_process_message (struct client *c,
+ struct msg_unpacker *r, struct relay_command_message *m)
+{
+ if (!relay_command_message_deserialize (m, r)
+ || msg_unpacker_get_available (r))
+ {
+ log_global_error (c->ctx, "Deserialization failed, killing client");
+ return false;
+ }
+
+ const char *buffer_name = client_message_buffer_name (m);
+ struct buffer *buffer = NULL;
+ if (buffer_name && !(buffer = buffer_by_name (c->ctx, buffer_name)))
+ {
+ relay_prepare_error (c->ctx, m->command_seq, "Unknown buffer");
+ relay_send (c);
+ return true;
+ }
+
+ switch (m->data.command)
+ {
+ case RELAY_COMMAND_HELLO:
+ if (m->data.hello.version != RELAY_VERSION)
+ {
+ // TODO: This should send back an error message and shut down.
+ log_global_error (c->ctx,
+ "Protocol version mismatch, killing client");
+ return false;
+ }
+ c->initialized = true;
+ client_resync (c);
+ break;
+ case RELAY_COMMAND_PING:
+ relay_prepare_response (c->ctx, m->command_seq)
+ ->data.command = RELAY_COMMAND_PING;
+ relay_send (c);
+ break;
+ case RELAY_COMMAND_ACTIVE:
+ reset_autoaway (c->ctx);
+ break;
+ case RELAY_COMMAND_BUFFER_COMPLETE:
+ client_process_buffer_complete (c, m->command_seq, buffer,
+ &m->data.buffer_complete);
+ break;
+ case RELAY_COMMAND_BUFFER_ACTIVATE:
+ buffer_activate (c->ctx, buffer);
+ break;
+ case RELAY_COMMAND_BUFFER_INPUT:
+ process_input (c->ctx, buffer, m->data.buffer_input.text.str);
+ break;
+ case RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT:
+ buffer_toggle_unimportant (c->ctx, buffer);
+ break;
+ case RELAY_COMMAND_BUFFER_LOG:
+ client_process_buffer_log (c, m->command_seq, buffer);
+ break;
+ default:
+ log_global_debug (c->ctx, "Unhandled client command");
+ relay_prepare_error (c->ctx, m->command_seq, "Unknown command");
+ relay_send (c);
+ }
+ return true;
+}
+
+static bool
+client_process_buffer (struct client *c)
+{
+ struct str *buf = &c->read_buffer;
+ size_t offset = 0;
+ while (true)
+ {
+ uint32_t frame_len = 0;
+ struct msg_unpacker r =
+ msg_unpacker_make (buf->str + offset, buf->len - offset);
+ if (!msg_unpacker_u32 (&r, &frame_len))
+ break;
+
+ r.len = MIN (r.len, sizeof frame_len + frame_len);
+ if (msg_unpacker_get_available (&r) < frame_len)
+ break;
+
+ struct relay_command_message m = {};
+ bool ok = client_process_message (c, &r, &m);
+ relay_command_message_free (&m);
+ if (!ok)
+ return false;
+
+ offset += r.offset;
+ }
+
+ str_remove_slice (buf, 0, offset);
+ return true;
+}
+
+// --- Relay plumbing ----------------------------------------------------------
+
+static bool
+client_try_read (struct client *c)
+{
+ struct str *buf = &c->read_buffer;
+ ssize_t n_read;
+
+ while ((n_read = read (c->socket_fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */)) > 0)
+ {
+ buf->len += n_read;
+ if (!client_process_buffer (c))
+ break;
+ str_reserve (buf, 512);
+ }
+
+ if (n_read < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ return true;
+
+ log_global_debug (c->ctx,
+ "#s: #s: #l", __func__, "read", strerror (errno));
+ }
+
+ client_kill (c);
+ return false;
+}
+
+static bool
+client_try_write (struct client *c)
+{
+ struct str *buf = &c->write_buffer;
+ ssize_t n_written;
+
+ while (buf->len)
+ {
+ n_written = write (c->socket_fd, buf->str, buf->len);
+ if (n_written >= 0)
+ {
+ str_remove_slice (buf, 0, n_written);
+ continue;
+ }
+ if (errno == EAGAIN || errno == EINTR)
+ return true;
+
+ log_global_debug (c->ctx,
+ "#s: #s: #l", __func__, "write", strerror (errno));
+ client_kill (c);
+ return false;
+ }
+ return true;
+}
+
+static void
+on_client_ready (const struct pollfd *pfd, void *user_data)
+{
+ struct client *c = user_data;
+ if (client_try_read (c) && client_try_write (c))
+ client_update_poller (c, pfd);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+relay_try_fetch_client (struct app_context *ctx, int listen_fd)
+{
+ // XXX: `struct sockaddr_storage' is not the most portable thing
+ struct sockaddr_storage peer;
+ socklen_t peer_len = sizeof peer;
+
+ int fd = accept (listen_fd, (struct sockaddr *) &peer, &peer_len);
+ if (fd == -1)
+ {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return false;
+ if (errno == EINTR)
+ return true;
+
+ if (accept_error_is_transient (errno))
+ {
+ log_global_debug (ctx, "#s: #l", "accept", strerror (errno));
+ return true;
+ }
+
+ log_global_error (ctx, "#s: #l", "accept", strerror (errno));
+ app_context_relay_stop (ctx);
+ return false;
+ }
+
+ hard_assert (peer_len <= sizeof peer);
+ set_blocking (fd, false);
+ set_cloexec (fd);
+
+ // We already buffer our output, so reduce latencies.
+ int yes = 1;
+ soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
+ &yes, sizeof yes) != -1);
+
+ struct client *c = client_new ();
+ c->ctx = ctx;
+ c->socket_fd = fd;
+ LIST_PREPEND (ctx->clients, c);
+
+ c->socket_event = poller_fd_make (&c->ctx->poller, c->socket_fd);
+ c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
+ c->socket_event.user_data = c;
+
+ client_update_poller (c, NULL);
+ return true;
+}
+
+static void
+on_relay_client_available (const struct pollfd *pfd, void *user_data)
+{
+ struct app_context *ctx = user_data;
+ while (relay_try_fetch_client (ctx, pfd->fd))
+ ;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+relay_listen (struct addrinfo *ai, struct error **e)
+{
+ int fd = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (fd == -1)
+ {
+ error_set (e, "socket: %s", strerror (errno));
+ return -1;
+ }
+
+ set_cloexec (fd);
+
+ int yes = 1;
+ soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
+ &yes, sizeof yes) != -1);
+ soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR,
+ &yes, sizeof yes) != -1);
+
+ if (bind (fd, ai->ai_addr, ai->ai_addrlen))
+ error_set (e, "bind: %s", strerror (errno));
+ else if (listen (fd, 16 /* arbitrary number */))
+ error_set (e, "listen: %s", strerror (errno));
+ else
+ return fd;
+
+ xclose (fd);
+ return -1;
+}
+
+static int
+relay_listen_with_context (struct app_context *ctx, struct addrinfo *ai,
+ struct error **e)
+{
+ char *address = gai_reconstruct_address (ai);
+ log_global_debug (ctx, "binding to `#l'", address);
+
+ struct error *error = NULL;
+ int fd = relay_listen (ai, &error);
+ if (fd == -1)
+ {
+ error_set (e, "binding to `%s' failed: %s", address, error->message);
+ error_free (error);
+ }
+ free (address);
+ return fd;
+}
+
+static bool
+relay_start (struct app_context *ctx, char *address, struct error **e)
+{
+ const char *port = NULL, *host = tokenize_host_port (address, &port);
+ if (!port || !*port)
+ return error_set (e, "missing port");
+
+ struct addrinfo hints = {}, *result = NULL;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+
+ int err = getaddrinfo (*host ? host : NULL, port, &hints, &result);
+ if (err)
+ {
+ return error_set (e, "failed to resolve `%s', port `%s': %s: %s",
+ host, port, "getaddrinfo", gai_strerror (err));
+ }
+
+ // Just try the first one, disregarding IPv4/IPv6 ordering.
+ int fd = relay_listen_with_context (ctx, result, e);
+ freeaddrinfo (result);
+ if (fd == -1)
+ return false;
+
+ set_blocking (fd, false);
+
+ struct poller_fd *event = &ctx->relay_event;
+ *event = poller_fd_make (&ctx->poller, fd);
+ event->dispatcher = (poller_fd_fn) on_relay_client_available;
+ event->user_data = ctx;
+
+ ctx->relay_fd = fd;
+ poller_fd_set (event, POLLIN);
+ return true;
+}
+
+static void
+on_config_relay_bind_change (struct config_item *item)
+{
+ struct app_context *ctx = item->user_data;
+ char *value = item->value.string.str;
+ app_context_relay_stop (ctx);
+ if (!value)
+ return;
+
+ // XXX: This should perhaps be reencoded as the locale encoding.
+ char *address = xstrdup (value);
+
+ struct error *e = NULL;
+ if (!relay_start (ctx, address, &e))
+ {
+ log_global_error (ctx, "#s: #l", item->schema->name, e->message);
+ error_free (e);
+ }
+ free (address);
+}
+
+// --- Tests -------------------------------------------------------------------
+
+// The application is quite monolithic and can only be partially unit-tested.
+// Locale-, terminal- and filesystem-dependent tests are also somewhat tricky.
+
+#ifdef TESTING
+
+static struct config_schema g_config_test[] =
+{
+ { .name = "foo", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" },
+ { .name = "bar", .type = CONFIG_ITEM_INTEGER, .default_ = "1" },
+ { .name = "foobar", .type = CONFIG_ITEM_STRING, .default_ = "\"x\\x01\"" },
+ {}
+};
+
+static void
+test_config (void)
+{
+ struct config_item *foo = config_item_object ();
+ config_schema_apply_to_object (g_config_test, foo, NULL);
+ struct config_item *root = config_item_object ();
+ str_map_set (&root->value.object, "top", foo);
+
+ struct strv v = strv_make ();
+ dump_matching_options (root, "*foo*", &v);
+ hard_assert (v.len == 2);
+ hard_assert (!strcmp (v.vector[0], "top.foo = off"));
+ hard_assert (!strcmp (v.vector[1], "top.foobar = \"x\\x01\""));
+ strv_free (&v);
+
+ config_item_destroy (root);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+test_aliases (void)
+{
+ struct strv v = strv_make ();
+ expand_alias_definition ("/foo; /bar $* $$$;;;$1$2$3$4", "foo bar baz", &v);
+ hard_assert (v.len == 4);
+ hard_assert (!strcmp (v.vector[0], "/foo"));
+ hard_assert (!strcmp (v.vector[1], " /bar foo bar baz $;"));
+ hard_assert (!strcmp (v.vector[2], ""));
+ hard_assert (!strcmp (v.vector[3], "foobarbaz"));
+ strv_free (&v);
+}
+
+static void
+test_wrapping (void)
+{
+ static const char *message = " foo bar foobar fóóbárbáz\002 a\0031 b";
+ // XXX: formatting continuation order is implementation-dependent here
+ // (irc_serialize_char_attrs() makes a choice in serialization)
+ static const char *split[] = { " foo", "bar", "foob", "ar",
+ "fó", "ób", "árb", "áz\x02", "\002a\0031", "\0031\002b" };
+
+ struct strv v = strv_make ();
+ hard_assert (wrap_message (message, 4, &v, NULL));
+ hard_assert (v.len == N_ELEMENTS (split));
+ for (size_t i = 0; i < N_ELEMENTS (split); i++)
+ hard_assert (!strcmp (v.vector[i], split[i]));
+ strv_free (&v);
+}
+
+static void
+test_utf8 (void)
+{
+ static const char *a[] = { "fřoo", "Fřooř", "fřOOŘ" };
+ hard_assert (utf8_common_prefix (a, N_ELEMENTS (a)) == 5);
+
+ char *cut_off = xstrdup ("ё\xD0");
+ irc_sanitize_cut_off_utf8 (&cut_off);
+ hard_assert (!strcmp (cut_off, "ё\xEF\xBF\xBD"));
+ free (cut_off);
+}
+
+int
+main (int argc, char *argv[])
+{
+ struct test test;
+ test_init (&test, argc, argv);
+ test_add_simple (&test, "/config", NULL, test_config);
+ test_add_simple (&test, "/aliases", NULL, test_aliases);
+ test_add_simple (&test, "/wrapping", NULL, test_wrapping);
+ test_add_simple (&test, "/utf8", NULL, test_utf8);
+ return test_run (&test);
+}
+
+#define main main_shadowed
+#endif // TESTING
+
+// --- Main program ------------------------------------------------------------
+
+static const char *g_logo[] =
+{
+ "",
+ "\x02" PROGRAM_NAME "\x02 " PROGRAM_VERSION,
+ "",
+};
+
+static void
+show_logo (struct app_context *ctx)
+{
+ for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
+ log_global_indent (ctx, "#m", g_logo[i]);
+}
+
+static void
+format_input_and_die (struct app_context *ctx)
+{
+ char buf[513];
+ while (fgets (buf, sizeof buf, stdin))
+ {
+ struct formatter f = formatter_make (ctx, NULL);
+ formatter_add (&f, "#m", buf);
+ formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
+ formatter_free (&f);
+ }
+ exit (EXIT_SUCCESS);
+}
+
+int
+main (int argc, char *argv[])
+{
+ // We include a generated file from xD including this array we don't use;
+ // let's just keep it there and silence the compiler warning instead
+ (void) g_default_replies;
+
+ static const struct opt opts[] =
+ {
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ // This is mostly intended for previewing formatted MOTD files
+ { 'f', "format", NULL, OPT_LONG_ONLY, "format IRC text from stdin" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh =
+ opt_handler_make (argc, argv, opts, NULL, "Terminal-based IRC client.");
+ bool format_mode = false;
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ case 'f':
+ format_mode = true;
+ break;
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+ if (optind != argc)
+ {
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+ opt_handler_free (&oh);
+
+ // We only need to convert to and from the terminal encoding
+ setlocale (LC_CTYPE, "");
+
+ struct app_context ctx;
+ app_context_init (&ctx);
+ g_ctx = &ctx;
+
+ init_openssl ();
+
+ // Bootstrap configuration, so that we can access schema items at all
+ register_config_modules (&ctx);
+ config_load (&ctx.config, config_item_object ());
+
+ // The following part is a bit brittle because of interdependencies
+ init_colors (&ctx);
+ if (format_mode) format_input_and_die (&ctx);
+ init_global_buffer (&ctx);
+ show_logo (&ctx);
+ setup_signal_handlers ();
+ init_poller_events (&ctx);
+ load_configuration (&ctx);
+
+ // At this moment we can safely call any "on_change" callbacks
+ config_schema_call_changed (ctx.config.root);
+
+ // Initialize input so that we can switch to new buffers
+ on_refresh_prompt (&ctx);
+ ctx.input->add_functions = input_add_functions;
+ CALL_ (ctx.input, start, argv[0]);
+ toggle_bracketed_paste (true);
+ reset_autoaway (&ctx);
+
+ // Finally, we juice the configuration for some servers to create
+ load_plugins (&ctx);
+ load_servers (&ctx);
+
+ ctx.polling = true;
+ while (ctx.polling)
+ poller_run (&ctx.poller);
+
+ CALL (ctx.input, stop);
+
+ app_context_free (&ctx);
+ toggle_bracketed_paste (false);
+ free_terminal ();
+ return EXIT_SUCCESS;
+}
diff --git a/xC.webp b/xC.webp
new file mode 100644
index 0000000..a10da64
--- /dev/null
+++ b/xC.webp
Binary files differ
diff --git a/xD-gen-replies.awk b/xD-gen-replies.awk
new file mode 100755
index 0000000..c9e8882
--- /dev/null
+++ b/xD-gen-replies.awk
@@ -0,0 +1,29 @@
+#!/usr/bin/awk -f
+BEGIN {
+ # The message catalog is a by-product
+ msg = "xD.msg"
+ print "$quote \"" > msg;
+ print "$set 1" > msg;
+}
+
+/^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
+ match($0, /".*"/);
+ ids[$1] = $2;
+ texts[$2] = substr($0, RSTART, RLENGTH);
+ print $1 " " texts[$2] > msg
+}
+
+END {
+ printf("enum\n{")
+ for (i in ids) {
+ if (seen_first)
+ printf(",")
+ seen_first = 1
+ printf("\n\t%s = %s", ids[i], i)
+ }
+ print "\n};\n"
+ print "static const char *g_default_replies[] =\n{"
+ for (i in ids)
+ print "\t[" ids[i] "] = " texts[ids[i]] ","
+ print "};"
+}
diff --git a/xD-replies b/xD-replies
new file mode 100644
index 0000000..fc8d6df
--- /dev/null
+++ b/xD-replies
@@ -0,0 +1,93 @@
+1 IRC_RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s"
+2 IRC_RPL_YOURHOST ":Your host is %s, running version %s"
+3 IRC_RPL_CREATED ":This server was created %s"
+4 IRC_RPL_MYINFO "%s %s %s %s"
+5 IRC_RPL_ISUPPORT "%s :are supported by this server"
+211 IRC_RPL_STATSLINKINFO "%s %zu %zu %zu %zu %zu %lld"
+212 IRC_RPL_STATSCOMMANDS "%s %zu %zu %zu"
+219 IRC_RPL_ENDOFSTATS "%c :End of STATS report"
+221 IRC_RPL_UMODEIS "+%s"
+242 IRC_RPL_STATSUPTIME ":Server Up %d days %d:%02d:%02d"
+251 IRC_RPL_LUSERCLIENT ":There are %d users and %d services on %d servers"
+252 IRC_RPL_LUSEROP "%d :operator(s) online"
+253 IRC_RPL_LUSERUNKNOWN "%d :unknown connection(s)"
+254 IRC_RPL_LUSERCHANNELS "%d :channels formed"
+255 IRC_RPL_LUSERME ":I have %d clients and %d servers"
+301 IRC_RPL_AWAY "%s :%s"
+302 IRC_RPL_USERHOST ":%s"
+303 IRC_RPL_ISON ":%s"
+305 IRC_RPL_UNAWAY ":You are no longer marked as being away"
+306 IRC_RPL_NOWAWAY ":You have been marked as being away"
+311 IRC_RPL_WHOISUSER "%s %s %s * :%s"
+312 IRC_RPL_WHOISSERVER "%s %s :%s"
+313 IRC_RPL_WHOISOPERATOR "%s :is an IRC operator"
+314 IRC_RPL_WHOWASUSER "%s %s %s * :%s"
+315 IRC_RPL_ENDOFWHO "%s :End of WHO list"
+317 IRC_RPL_WHOISIDLE "%s %d :seconds idle"
+318 IRC_RPL_ENDOFWHOIS "%s :End of WHOIS list"
+319 IRC_RPL_WHOISCHANNELS "%s :%s"
+322 IRC_RPL_LIST "%s %d :%s"
+323 IRC_RPL_LISTEND ":End of LIST"
+324 IRC_RPL_CHANNELMODEIS "%s +%s"
+329 IRC_RPL_CREATIONTIME "%s %lld"
+331 IRC_RPL_NOTOPIC "%s :No topic is set"
+332 IRC_RPL_TOPIC "%s :%s"
+333 IRC_RPL_TOPICWHOTIME "%s %s %lld"
+341 IRC_RPL_INVITING "%s %s"
+346 IRC_RPL_INVITELIST "%s %s"
+347 IRC_RPL_ENDOFINVITELIST "%s :End of channel invite list"
+348 IRC_RPL_EXCEPTLIST "%s %s"
+349 IRC_RPL_ENDOFEXCEPTLIST "%s :End of channel exception list"
+351 IRC_RPL_VERSION "%s.%d %s :%s"
+352 IRC_RPL_WHOREPLY "%s %s %s %s %s %s :%d %s"
+353 IRC_RPL_NAMREPLY "%c %s :%s"
+364 IRC_RPL_LINKS "%s %s :%d %s"
+365 IRC_RPL_ENDOFLINKS "%s :End of LINKS list"
+366 IRC_RPL_ENDOFNAMES "%s :End of NAMES list"
+367 IRC_RPL_BANLIST "%s %s"
+368 IRC_RPL_ENDOFBANLIST "%s :End of channel ban list"
+369 IRC_RPL_ENDOFWHOWAS "%s :End of WHOWAS"
+372 IRC_RPL_MOTD ":- %s"
+375 IRC_RPL_MOTDSTART ":- %s Message of the day - "
+376 IRC_RPL_ENDOFMOTD ":End of MOTD command"
+391 IRC_RPL_TIME "%s :%s"
+401 IRC_ERR_NOSUCHNICK "%s :No such nick/channel"
+402 IRC_ERR_NOSUCHSERVER "%s :No such server"
+403 IRC_ERR_NOSUCHCHANNEL "%s :No such channel"
+404 IRC_ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel"
+406 IRC_ERR_WASNOSUCHNICK "%s :There was no such nickname"
+409 IRC_ERR_NOORIGIN ":No origin specified"
+410 IRC_ERR_INVALIDCAPCMD "%s :%s"
+411 IRC_ERR_NORECIPIENT ":No recipient given (%s)"
+412 IRC_ERR_NOTEXTTOSEND ":No text to send"
+421 IRC_ERR_UNKNOWNCOMMAND "%s: Unknown command"
+422 IRC_ERR_NOMOTD ":MOTD File is missing"
+423 IRC_ERR_NOADMININFO "%s :No administrative info available"
+431 IRC_ERR_NONICKNAMEGIVEN ":No nickname given"
+432 IRC_ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname"
+433 IRC_ERR_NICKNAMEINUSE "%s :Nickname is already in use"
+441 IRC_ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel"
+442 IRC_ERR_NOTONCHANNEL "%s :You're not on that channel"
+443 IRC_ERR_USERONCHANNEL "%s %s :is already on channel"
+445 IRC_ERR_SUMMONDISABLED ":SUMMON has been disabled"
+446 IRC_ERR_USERSDISABLED ":USERS has been disabled"
+451 IRC_ERR_NOTREGISTERED ":You have not registered"
+461 IRC_ERR_NEEDMOREPARAMS "%s :Not enough parameters"
+462 IRC_ERR_ALREADYREGISTERED ":Unauthorized command (already registered)"
+467 IRC_ERR_KEYSET "%s :Channel key already set"
+471 IRC_ERR_CHANNELISFULL "%s :Cannot join channel (+l)"
+472 IRC_ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s"
+473 IRC_ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)"
+474 IRC_ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)"
+475 IRC_ERR_BADCHANNELKEY "%s :Cannot join channel (+k)"
+476 IRC_ERR_BADCHANMASK "%s :Bad Channel Mask"
+481 IRC_ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator"
+482 IRC_ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
+501 IRC_ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
+502 IRC_ERR_USERSDONTMATCH ":Cannot change mode for other users"
+902 IRC_ERR_NICKLOCKED ":You must use a nick assigned to you"
+903 IRC_RPL_SASLSUCCESS ":SASL authentication successful"
+904 IRC_ERR_SASLFAIL ":SASL authentication failed"
+905 IRC_ERR_SASLTOOLONG ":SASL message too long"
+906 IRC_ERR_SASLABORTED ":SASL authentication aborted"
+907 IRC_ERR_SASLALREADY ":You have already authenticated using SASL"
diff --git a/xD.adoc b/xD.adoc
new file mode 100644
index 0000000..14012b0
--- /dev/null
+++ b/xD.adoc
@@ -0,0 +1,53 @@
+xD(1)
+=====
+:doctype: manpage
+:manmanual: xK Manual
+:mansource: xK {release-version}
+
+Name
+----
+xD - IRC daemon
+
+Synopsis
+--------
+*xD* [_OPTION_]...
+
+Description
+-----------
+*xD* is a basic IRC daemon for single-server networks, suitable for testing
+and private use. When run without a configuration file, it will start listening
+on the standard port 6667 and the "any" address.
+
+Options
+-------
+*-d*, *--debug*::
+ Do not daemonize, print more information on the standard error stream
+ to help debug various issues.
+
+*-h*, *--help*::
+ Display a help message and exit.
+
+*-V*, *--version*::
+ Output version information and exit.
+
+*--write-default-cfg*[**=**__PATH__]::
+ Write a configuration file with defaults, show its path and exit.
++
+The file will be appropriately commented.
++
+When no _PATH_ is specified, it will be created in the user's home directory,
+contrary to what you might expect from a server.
+
+Files
+-----
+*xD* follows the XDG Base Directory Specification.
+
+_~/.config/xD/xD.conf_::
+_/etc/xdg/xD/xD.conf_::
+ The daemon's configuration file. Use the *--write-default-cfg* option
+ to create a new one for editing.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/xK to report bugs, request features,
+or submit pull requests.
diff --git a/xD.c b/xD.c
new file mode 100644
index 0000000..65ebe7e
--- /dev/null
+++ b/xD.c
@@ -0,0 +1,4106 @@
+/*
+ * xD.c: an IRC daemon
+ *
+ * Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ */
+
+#include "config.h"
+#define PROGRAM_NAME "xD"
+
+#define WANT_SYSLOG_LOGGING
+#include "common.c"
+#include "xD-replies.c"
+
+#include <nl_types.h>
+#include <sys/resource.h>
+
+enum { PIPE_READ, PIPE_WRITE };
+
+// FIXME: don't use time_t to compute time deltas
+
+// --- Configuration (application-specific) ------------------------------------
+
+// Just get rid of the crappiest ciphers available by default
+#define DEFAULT_CIPHERS "DEFAULT:!MEDIUM:!LOW"
+
+static struct simple_config_item g_config_table[] =
+{
+ { "pid_file", NULL, "Path or name of the PID file" },
+ { "server_name", NULL, "Server name" },
+ { "server_info", "My server", "Brief server description" },
+ { "motd", NULL, "MOTD filename" },
+ { "catalog", NULL, "catgets localization catalog" },
+
+ { "bind_host", NULL, "Address of the IRC server" },
+ { "bind_port", "6667", "Port of the IRC server" },
+ { "tls_cert", NULL, "Server TLS certificate (PEM)" },
+ { "tls_key", NULL, "Server TLS private key (PEM)" },
+ { "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
+
+ { "operators", NULL, "IRCop TLS client cert. SHA-1 fingerprints" },
+
+ { "max_connections", "0", "Global connection limit" },
+ { "ping_interval", "180", "Interval between PINGs (sec)" },
+ { NULL, NULL, NULL }
+};
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+/// Program termination has been requested by a signal
+static volatile sig_atomic_t g_termination_requested;
+
+static void
+sigterm_handler (int signum)
+{
+ (void) signum;
+
+ g_termination_requested = true;
+
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], "t", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+setup_signal_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ signal (SIGPIPE, SIG_IGN);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sigemptyset (&sa.sa_mask);
+ sa.sa_handler = sigterm_handler;
+ if (sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1)
+ exit_fatal ("%s: %s", "sigaction", strerror (errno));
+}
+
+// --- Rate limiter ------------------------------------------------------------
+
+struct flood_detector
+{
+ unsigned interval; ///< Interval for the limit
+ unsigned limit; ///< Maximum number of events allowed
+
+ time_t *timestamps; ///< Timestamps of last events
+ unsigned pos; ///< Index of the oldest event
+};
+
+static void
+flood_detector_init (struct flood_detector *self,
+ unsigned interval, unsigned limit)
+{
+ self->interval = interval;
+ self->limit = limit;
+ self->timestamps = xcalloc (limit + 1, sizeof *self->timestamps);
+ self->pos = 0;
+}
+
+static void
+flood_detector_free (struct flood_detector *self)
+{
+ free (self->timestamps);
+}
+
+static bool
+flood_detector_check (struct flood_detector *self)
+{
+ time_t now = time (NULL);
+ self->timestamps[self->pos++] = now;
+ if (self->pos > self->limit)
+ self->pos = 0;
+
+ time_t begin = now - self->interval;
+ size_t count = 0;
+ for (size_t i = 0; i <= self->limit; i++)
+ if (self->timestamps[i] >= begin)
+ count++;
+ return count <= self->limit;
+}
+
+// --- IRC token validation ----------------------------------------------------
+
+// Use the enum only if applicable and a simple boolean isn't sufficient.
+
+enum validation_result
+{
+ VALIDATION_OK,
+ VALIDATION_ERROR_EMPTY,
+ VALIDATION_ERROR_TOO_LONG,
+ VALIDATION_ERROR_INVALID
+};
+
+// Everything as per RFC 2812
+#define IRC_MAX_NICKNAME 9
+#define IRC_MAX_HOSTNAME 63
+#define IRC_MAX_CHANNEL_NAME 50
+#define IRC_MAX_MESSAGE_LENGTH 510
+
+static bool
+irc_regex_match (const char *regex, const char *s)
+{
+ static struct str_map cache;
+ static bool initialized;
+
+ if (!initialized)
+ {
+ cache = regex_cache_make ();
+ initialized = true;
+ }
+
+ struct error *e = NULL;
+ bool result = regex_cache_match (&cache, regex,
+ REG_EXTENDED | REG_NOSUB, s, &e);
+ hard_assert (!e);
+ return result;
+}
+
+static const char *
+irc_validate_to_str (enum validation_result result)
+{
+ switch (result)
+ {
+ case VALIDATION_OK: return "success";
+ case VALIDATION_ERROR_EMPTY: return "the value is empty";
+ case VALIDATION_ERROR_INVALID: return "invalid format";
+ case VALIDATION_ERROR_TOO_LONG: return "the value is too long";
+ default: abort ();
+ }
+}
+
+// Anything to keep it as short as possible
+// "shortname" from RFC 2812 doesn't work how its author thought it would.
+#define SN "[0-9A-Za-z](-*[0-9A-Za-z])*"
+#define N4 "[0-9]{1,3}"
+#define N6 "[0-9ABCDEFabcdef]{1,}"
+
+#define LE "A-Za-z"
+#define SP "][\\\\`_^{|}"
+
+static enum validation_result
+irc_validate_hostname (const char *hostname)
+{
+ if (!*hostname)
+ return VALIDATION_ERROR_EMPTY;
+ if (!irc_regex_match ("^" SN "(\\." SN ")*$", hostname))
+ return VALIDATION_ERROR_INVALID;
+ if (strlen (hostname) > IRC_MAX_HOSTNAME)
+ return VALIDATION_ERROR_TOO_LONG;
+ return VALIDATION_OK;
+}
+
+static bool
+irc_is_valid_hostaddr (const char *hostaddr)
+{
+ if (irc_regex_match ("^" N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr)
+ || irc_regex_match ("^" N6 ":" N6 ":" N6 ":" N6 ":"
+ N6 ":" N6 ":" N6 ":" N6 "$", hostaddr)
+ || irc_regex_match ("^0:0:0:0:0:(0|[Ff]{4}):"
+ N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr))
+ return true;
+ return false;
+}
+
+// TODO: we should actually use this, though what should we do on failure?
+static bool
+irc_is_valid_host (const char *host)
+{
+ return irc_validate_hostname (host) == VALIDATION_OK
+ || irc_is_valid_hostaddr (host);
+}
+
+// TODO: currently, we are almost encoding-agnostic (strings just need to be
+// ASCII-compatible). We should at least have an option to enforce a specific
+// encoding, such as UTF-8. Note that with Unicode we should not allow all
+// character clasess and exclude the likes of \pM with the goal of enforcing
+// NFC-normalized identifiers--utf8proc is a good candidate library to handle
+// the categorization and validation.
+
+static bool
+irc_is_valid_user (const char *user)
+{
+ return irc_regex_match ("^[^\r\n @]+$", user);
+}
+
+static bool
+irc_validate_nickname (const char *nickname)
+{
+ if (!*nickname)
+ return VALIDATION_ERROR_EMPTY;
+ if (!irc_regex_match ("^[" SP LE "][" SP LE "0-9-]*$", nickname))
+ return VALIDATION_ERROR_INVALID;
+ if (strlen (nickname) > IRC_MAX_NICKNAME)
+ return VALIDATION_ERROR_TOO_LONG;
+ return VALIDATION_OK;
+}
+
+static enum validation_result
+irc_validate_channel_name (const char *channel_name)
+{
+ if (!*channel_name)
+ return VALIDATION_ERROR_EMPTY;
+ if (*channel_name != '#' || strpbrk (channel_name, "\7\r\n ,:"))
+ return VALIDATION_ERROR_INVALID;
+ if (strlen (channel_name) > IRC_MAX_CHANNEL_NAME)
+ return VALIDATION_ERROR_TOO_LONG;
+ return VALIDATION_OK;
+}
+
+static bool
+irc_is_valid_key (const char *key)
+{
+ // XXX: should be 7-bit as well but whatever
+ return irc_regex_match ("^[^\r\n\f\t\v ]{1,23}$", key);
+}
+
+#undef SN
+#undef N4
+#undef N6
+
+#undef LE
+#undef SP
+
+static bool
+irc_is_valid_user_mask (const char *mask)
+{
+ return irc_regex_match ("^[^!@]+![^!@]+@[^@!]+$", mask);
+}
+
+static bool
+irc_is_valid_fingerprint (const char *fp)
+{
+ return irc_regex_match ("^[a-fA-F0-9]{40}$", fp);
+}
+
+// --- Clients (equals users) --------------------------------------------------
+
+#define IRC_SUPPORTED_USER_MODES "aiwros"
+
+enum
+{
+ IRC_USER_MODE_INVISIBLE = (1 << 0),
+ IRC_USER_MODE_RX_WALLOPS = (1 << 1),
+ IRC_USER_MODE_RESTRICTED = (1 << 2),
+ IRC_USER_MODE_OPERATOR = (1 << 3),
+ IRC_USER_MODE_RX_SERVER_NOTICES = (1 << 4)
+};
+
+enum
+{
+ IRC_CAP_MULTI_PREFIX = (1 << 0),
+ IRC_CAP_INVITE_NOTIFY = (1 << 1),
+ IRC_CAP_ECHO_MESSAGE = (1 << 2),
+ IRC_CAP_USERHOST_IN_NAMES = (1 << 3),
+ IRC_CAP_SERVER_TIME = (1 << 4)
+};
+
+struct client
+{
+ LIST_HEADER (struct client)
+ struct server_context *ctx; ///< Server context
+
+ time_t opened; ///< When the connection was opened
+ size_t n_sent_messages; ///< Number of sent messages total
+ size_t sent_bytes; ///< Number of sent bytes total
+ size_t n_received_messages; ///< Number of received messages total
+ size_t received_bytes; ///< Number of received bytes total
+
+ int socket_fd; ///< The TCP socket
+ struct str read_buffer; ///< Unprocessed input
+ struct str write_buffer; ///< Output yet to be sent out
+
+ struct poller_fd socket_event; ///< The socket can be read/written to
+ struct poller_timer ping_timer; ///< We should send a ping
+ struct poller_timer timeout_timer; ///< Connection seems to be dead
+ struct poller_timer kill_timer; ///< Hard kill timeout
+
+ unsigned long cap_version; ///< CAP protocol version
+ unsigned caps_enabled; ///< Enabled capabilities
+
+ unsigned initialized : 1; ///< Has any data been received yet?
+ unsigned cap_negotiating : 1; ///< Negotiating capabilities
+ unsigned registered : 1; ///< The user has registered
+ unsigned closing_link : 1; ///< Closing link
+ unsigned half_closed : 1; ///< Closing link: conn. is half-closed
+
+ unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write
+ unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read
+ SSL *ssl; ///< SSL connection
+ char *ssl_cert_fingerprint; ///< Client certificate fingerprint
+
+ char *nickname; ///< IRC nickname (main identifier)
+ char *username; ///< IRC username
+ char *realname; ///< IRC realname (e-mail)
+
+ char *hostname; ///< Hostname shown to the network
+ char *port; ///< Port of the peer as a string
+ char *address; ///< Full address
+
+ unsigned mode; ///< User's mode
+ char *away_message; ///< Away message
+ time_t last_active; ///< Last PRIVMSG, to get idle time
+ struct str_map invites; ///< Channel invitations by operators
+ struct flood_detector antiflood; ///< Flood detector
+
+ struct async_getnameinfo *gni; ///< Backwards DNS resolution
+ struct poller_timer gni_timer; ///< Backwards DNS resolution timeout
+};
+
+static struct client *
+client_new (void)
+{
+ struct client *self = xcalloc (1, sizeof *self);
+ self->socket_fd = -1;
+ self->read_buffer = str_make ();
+ self->write_buffer = str_make ();
+ self->cap_version = 301;
+ // TODO: make this configurable and more fine-grained
+ flood_detector_init (&self->antiflood, 10, 20);
+ self->invites = str_map_make (NULL);
+ self->invites.key_xfrm = irc_strxfrm;
+ return self;
+}
+
+static void
+client_destroy (struct client *self)
+{
+ if (!soft_assert (self->socket_fd == -1))
+ xclose (self->socket_fd);
+ if (self->ssl)
+ SSL_free (self->ssl);
+
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+ free (self->ssl_cert_fingerprint);
+
+ free (self->nickname);
+ free (self->username);
+ free (self->realname);
+
+ free (self->hostname);
+ free (self->port);
+ free (self->address);
+
+ free (self->away_message);
+ flood_detector_free (&self->antiflood);
+ str_map_free (&self->invites);
+
+ if (self->gni)
+ async_cancel (&self->gni->async);
+ free (self);
+}
+
+static void client_close_link (struct client *c, const char *reason);
+static void client_kill (struct client *c, const char *reason);
+static void client_send (struct client *, const char *, ...)
+ ATTRIBUTE_PRINTF (2, 3);
+static void client_cancel_timers (struct client *);
+static void client_set_kill_timer (struct client *);
+static void client_update_poller (struct client *, const struct pollfd *);
+
+// --- Channels ----------------------------------------------------------------
+
+#define IRC_SUPPORTED_CHAN_MODES "ov" "beI" "imnqpst" "kl"
+
+enum
+{
+ IRC_CHAN_MODE_INVITE_ONLY = (1 << 0),
+ IRC_CHAN_MODE_MODERATED = (1 << 1),
+ IRC_CHAN_MODE_NO_OUTSIDE_MSGS = (1 << 2),
+ IRC_CHAN_MODE_QUIET = (1 << 3),
+ IRC_CHAN_MODE_PRIVATE = (1 << 4),
+ IRC_CHAN_MODE_SECRET = (1 << 5),
+ IRC_CHAN_MODE_PROTECTED_TOPIC = (1 << 6),
+
+ IRC_CHAN_MODE_OPERATOR = (1 << 7),
+ IRC_CHAN_MODE_VOICE = (1 << 8)
+};
+
+struct channel_user
+{
+ LIST_HEADER (struct channel_user)
+
+ unsigned modes;
+ struct client *c;
+};
+
+struct channel
+{
+ struct server_context *ctx; ///< Server context
+
+ char *name; ///< Channel name
+ unsigned modes; ///< Channel modes
+ char *key; ///< Channel key
+ long user_limit; ///< User limit or -1
+ time_t created; ///< Creation time
+
+ char *topic; ///< Channel topic
+ char *topic_who; ///< Who set the topic
+ time_t topic_time; ///< When the topic was set
+
+ struct channel_user *users; ///< Channel users
+
+ struct strv ban_list; ///< Ban list
+ struct strv exception_list; ///< Exceptions from bans
+ struct strv invite_list; ///< Exceptions from +I
+};
+
+static struct channel *
+channel_new (void)
+{
+ struct channel *self = xcalloc (1, sizeof *self);
+
+ self->user_limit = -1;
+ self->topic = xstrdup ("");
+
+ self->ban_list = strv_make ();
+ self->exception_list = strv_make ();
+ self->invite_list = strv_make ();
+ return self;
+}
+
+static void
+channel_delete (struct channel *self)
+{
+ free (self->name);
+ free (self->key);
+ free (self->topic);
+ free (self->topic_who);
+
+ struct channel_user *link, *tmp;
+ for (link = self->users; link; link = tmp)
+ {
+ tmp = link->next;
+ free (link);
+ }
+
+ strv_free (&self->ban_list);
+ strv_free (&self->exception_list);
+ strv_free (&self->invite_list);
+
+ free (self);
+}
+
+static char *
+channel_get_mode (struct channel *self, bool disclose_secrets)
+{
+ struct str mode = str_make ();
+ unsigned m = self->modes;
+ if (m & IRC_CHAN_MODE_INVITE_ONLY) str_append_c (&mode, 'i');
+ if (m & IRC_CHAN_MODE_MODERATED) str_append_c (&mode, 'm');
+ if (m & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) str_append_c (&mode, 'n');
+ if (m & IRC_CHAN_MODE_QUIET) str_append_c (&mode, 'q');
+ if (m & IRC_CHAN_MODE_PRIVATE) str_append_c (&mode, 'p');
+ if (m & IRC_CHAN_MODE_SECRET) str_append_c (&mode, 's');
+ if (m & IRC_CHAN_MODE_PROTECTED_TOPIC) str_append_c (&mode, 't');
+
+ if (self->user_limit != -1) str_append_c (&mode, 'l');
+ if (self->key) str_append_c (&mode, 'k');
+
+ // XXX: is it correct to split it? Try it on an existing implementation.
+ if (disclose_secrets)
+ {
+ if (self->user_limit != -1)
+ str_append_printf (&mode, " %ld", self->user_limit);
+ if (self->key)
+ str_append_printf (&mode, " %s", self->key);
+ }
+ return str_steal (&mode);
+}
+
+static struct channel_user *
+channel_get_user (const struct channel *chan, const struct client *c)
+{
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ if (iter->c == c)
+ return iter;
+ return NULL;
+}
+
+static struct channel_user *
+channel_add_user (struct channel *chan, struct client *c)
+{
+ struct channel_user *link = xcalloc (1, sizeof *link);
+ link->c = c;
+ LIST_PREPEND (chan->users, link);
+ return link;
+}
+
+static void
+channel_remove_user (struct channel *chan, struct channel_user *user)
+{
+ LIST_UNLINK (chan->users, user);
+ free (user);
+}
+
+static size_t
+channel_user_count (const struct channel *chan)
+{
+ size_t result = 0;
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ result++;
+ return result;
+}
+
+// --- IRC server context ------------------------------------------------------
+
+struct whowas_info
+{
+ char *nickname; ///< IRC nickname
+ char *username; ///< IRC username
+ char *realname; ///< IRC realname
+ char *hostname; ///< Hostname shown to the network
+};
+
+struct whowas_info *
+whowas_info_new (struct client *c)
+{
+ struct whowas_info *self = xmalloc (sizeof *self);
+ self->nickname = xstrdup (c->nickname);
+ self->username = xstrdup (c->username);
+ self->realname = xstrdup (c->realname);
+ self->hostname = xstrdup (c->hostname);
+ return self;
+}
+
+static void
+whowas_info_destroy (struct whowas_info *self)
+{
+ free (self->nickname);
+ free (self->username);
+ free (self->realname);
+ free (self->hostname);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct irc_command
+{
+ const char *name;
+ bool requires_registration;
+ void (*handler) (const struct irc_message *, struct client *);
+
+ size_t n_received; ///< Number of commands received
+ size_t bytes_received; ///< Number of bytes received total
+};
+
+struct server_context
+{
+ int *listen_fds; ///< Listening socket FD's
+ struct poller_fd *listen_events; ///< New connections available
+ size_t listen_len; ///< Number of listening sockets
+ size_t listen_alloc; ///< How many we've allocated
+
+ time_t started; ///< When has the server been started
+
+ SSL_CTX *ssl_ctx; ///< SSL context
+ struct client *clients; ///< Clients
+ unsigned n_clients; ///< Current number of connections
+
+ struct str_map users; ///< Maps nicknames to clients
+ struct str_map channels; ///< Maps channel names to data
+ struct str_map handlers; ///< Message handlers
+ struct str_map cap_handlers; ///< CAP message handlers
+
+ struct str_map whowas; ///< WHOWAS registry
+
+ struct poller poller; ///< Manages polled description
+ struct poller_timer quit_timer; ///< Quit timeout timer
+ bool quitting; ///< User requested quitting
+ bool polling; ///< The event loop is running
+
+ struct poller_fd signal_event; ///< Got a signal
+
+ struct str_map config; ///< Server configuration
+ char *server_name; ///< Our server name
+ unsigned ping_interval; ///< Ping interval in seconds
+ unsigned max_connections; ///< Max. connections allowed or 0
+ struct strv motd; ///< MOTD (none if empty)
+ nl_catd catalog; ///< Message catalog for server msgs
+ struct str_map operators; ///< TLS cert. fingerprints for IRCops
+};
+
+static void
+on_irc_quit_timeout (void *user_data)
+{
+ struct server_context *ctx = user_data;
+ struct client *iter, *next;
+ for (iter = ctx->clients; iter; iter = next)
+ {
+ next = iter->next;
+ // irc_initiate_quit() has already unregistered the client
+ client_kill (iter, "Shutting down");
+ }
+}
+
+static void
+server_context_init (struct server_context *self)
+{
+ memset (self, 0, sizeof *self);
+
+ self->users = str_map_make (NULL);
+ self->users.key_xfrm = irc_strxfrm;
+ self->channels = str_map_make ((str_map_free_fn) channel_delete);
+ self->channels.key_xfrm = irc_strxfrm;
+ self->handlers = str_map_make (NULL);
+ self->handlers.key_xfrm = irc_strxfrm;
+ self->cap_handlers = str_map_make (NULL);
+ self->cap_handlers.key_xfrm = irc_strxfrm;
+
+ self->whowas = str_map_make ((str_map_free_fn) whowas_info_destroy);
+ self->whowas.key_xfrm = irc_strxfrm;
+
+ poller_init (&self->poller);
+ self->quit_timer = poller_timer_make (&self->poller);
+ self->quit_timer.dispatcher = on_irc_quit_timeout;
+ self->quit_timer.user_data = self;
+
+ self->config = str_map_make (free);
+ simple_config_load_defaults (&self->config, g_config_table);
+
+ self->motd = strv_make ();
+ self->catalog = (nl_catd) -1;
+ self->operators = str_map_make (NULL);
+ // The regular irc_strxfrm() is sufficient for fingerprints
+ self->operators.key_xfrm = irc_strxfrm;
+}
+
+static void
+server_context_free (struct server_context *self)
+{
+ str_map_free (&self->config);
+
+ for (size_t i = 0; i < self->listen_len; i++)
+ {
+ poller_fd_reset (&self->listen_events[i]);
+ xclose (self->listen_fds[i]);
+ }
+ free (self->listen_fds);
+ free (self->listen_events);
+
+ hard_assert (!self->clients);
+ if (self->ssl_ctx)
+ SSL_CTX_free (self->ssl_ctx);
+
+ free (self->server_name);
+ str_map_free (&self->users);
+ str_map_free (&self->channels);
+ str_map_free (&self->handlers);
+ str_map_free (&self->cap_handlers);
+ str_map_free (&self->whowas);
+ poller_free (&self->poller);
+
+ strv_free (&self->motd);
+ if (self->catalog != (nl_catd) -1)
+ catclose (self->catalog);
+ str_map_free (&self->operators);
+}
+
+static const char *
+irc_get_text (struct server_context *ctx, int id, const char *def)
+{
+ if (!soft_assert (def != NULL))
+ def = "";
+ if (ctx->catalog == (nl_catd) -1)
+ return def;
+ return catgets (ctx->catalog, 1, id, def);
+}
+
+static void
+irc_try_finish_quit (struct server_context *ctx)
+{
+ if (!ctx->n_clients && ctx->quitting)
+ {
+ poller_timer_reset (&ctx->quit_timer);
+ ctx->polling = false;
+ }
+}
+
+static void
+irc_initiate_quit (struct server_context *ctx)
+{
+ print_status ("shutting down");
+
+ for (size_t i = 0; i < ctx->listen_len; i++)
+ {
+ poller_fd_reset (&ctx->listen_events[i]);
+ xclose (ctx->listen_fds[i]);
+ }
+ ctx->listen_len = 0;
+
+ for (struct client *iter = ctx->clients; iter; iter = iter->next)
+ if (!iter->closing_link)
+ client_close_link (iter, "Shutting down");
+
+ ctx->quitting = true;
+ poller_timer_set (&ctx->quit_timer, 5000);
+ irc_try_finish_quit (ctx);
+}
+
+static struct channel *
+irc_channel_create (struct server_context *ctx, const char *name)
+{
+ struct channel *chan = channel_new ();
+ chan->ctx = ctx;
+ chan->name = xstrdup (name);
+ chan->created = time (NULL);
+ str_map_set (&ctx->channels, name, chan);
+ return chan;
+}
+
+static void
+irc_channel_destroy_if_empty (struct server_context *ctx, struct channel *chan)
+{
+ if (!chan->users)
+ str_map_set (&ctx->channels, chan->name, NULL);
+}
+
+static void
+irc_send_to_roommates (struct client *c, const char *message)
+{
+ struct str_map targets = str_map_make (NULL);
+ targets.key_xfrm = irc_strxfrm;
+
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ struct channel *chan;
+ while ((chan = str_map_iter_next (&iter)))
+ {
+ if (chan->modes & IRC_CHAN_MODE_QUIET
+ || !channel_get_user (chan, c))
+ continue;
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ str_map_set (&targets, iter->c->nickname, iter->c);
+ }
+
+ iter = str_map_iter_make (&targets);
+ struct client *target;
+ while ((target = str_map_iter_next (&iter)))
+ if (target != c)
+ client_send (target, "%s", message);
+ str_map_free (&targets);
+}
+
+// --- Clients (continued) -----------------------------------------------------
+
+static void
+client_mode_to_str (unsigned m, struct str *out)
+{
+ if (m & IRC_USER_MODE_INVISIBLE) str_append_c (out, 'i');
+ if (m & IRC_USER_MODE_RX_WALLOPS) str_append_c (out, 'w');
+ if (m & IRC_USER_MODE_RESTRICTED) str_append_c (out, 'r');
+ if (m & IRC_USER_MODE_OPERATOR) str_append_c (out, 'o');
+ if (m & IRC_USER_MODE_RX_SERVER_NOTICES) str_append_c (out, 's');
+}
+
+static char *
+client_get_mode (struct client *self)
+{
+ struct str mode = str_make ();
+ if (self->away_message)
+ str_append_c (&mode, 'a');
+ client_mode_to_str (self->mode, &mode);
+ return str_steal (&mode);
+}
+
+static void
+client_send_str (struct client *c, const struct str *s)
+{
+ hard_assert (!c->closing_link);
+
+ size_t old_sendq = c->write_buffer.len;
+
+ // So far there's only one message tag we use, so we can do it simple;
+ // note that a 1024-character limit applies to messages with tags on
+ if (c->caps_enabled & IRC_CAP_SERVER_TIME)
+ {
+ long milliseconds; char buf[32]; struct tm tm;
+ time_t now = unixtime_msec (&milliseconds);
+ if (soft_assert (strftime (buf, sizeof buf,
+ "%Y-%m-%dT%T", gmtime_r (&now, &tm))))
+ str_append_printf (&c->write_buffer,
+ "@time=%s.%03ldZ ", buf, milliseconds);
+ }
+
+ // TODO: kill the connection above some "SendQ" threshold (careful!)
+ str_append_data (&c->write_buffer, s->str,
+ MIN (s->len, IRC_MAX_MESSAGE_LENGTH));
+ str_append (&c->write_buffer, "\r\n");
+ client_update_poller (c, NULL);
+
+ // Technically we haven't sent it yet but that's a minor detail
+ c->n_sent_messages++;
+ c->sent_bytes += c->write_buffer.len - old_sendq;
+}
+
+static void
+client_send (struct client *c, const char *format, ...)
+{
+ struct str tmp = str_make ();
+
+ va_list ap;
+ va_start (ap, format);
+ str_append_vprintf (&tmp, format, ap);
+ va_end (ap);
+
+ client_send_str (c, &tmp);
+ str_free (&tmp);
+}
+
+static void
+client_add_to_whowas (struct client *c)
+{
+ // Only keeping one entry for each nickname
+ // TODO: make sure this list doesn't get too long, for example by
+ // putting them in a linked list ordered by time
+ str_map_set (&c->ctx->whowas, c->nickname, whowas_info_new (c));
+}
+
+static void
+client_unregister (struct client *c, const char *reason)
+{
+ if (!c->registered)
+ return;
+
+ char *message = xstrdup_printf (":%s!%s@%s QUIT :%s",
+ c->nickname, c->username, c->hostname, reason);
+ irc_send_to_roommates (c, message);
+ free (message);
+
+ struct str_map_unset_iter iter =
+ str_map_unset_iter_make (&c->ctx->channels);
+ struct channel *chan;
+ while ((chan = str_map_unset_iter_next (&iter)))
+ {
+ struct channel_user *user;
+ if (!(user = channel_get_user (chan, c)))
+ continue;
+ channel_remove_user (chan, user);
+ irc_channel_destroy_if_empty (c->ctx, chan);
+ }
+ str_map_unset_iter_free (&iter);
+
+ client_add_to_whowas (c);
+
+ str_map_set (&c->ctx->users, c->nickname, NULL);
+ cstr_set (&c->nickname, NULL);
+ c->registered = false;
+}
+
+static void
+client_kill (struct client *c, const char *reason)
+{
+ struct server_context *ctx = c->ctx;
+ client_unregister (c, reason ? reason : "Client exited");
+
+ if (c->address)
+ // Only log the event if address resolution has finished
+ print_debug ("closed connection to %s (%s)", c->address,
+ reason ? reason : "");
+
+ if (c->ssl)
+ // Note that we might have already called this once, but that is fine
+ (void) SSL_shutdown (c->ssl);
+
+ xclose (c->socket_fd);
+ c->socket_fd = -1;
+
+ // We don't fork any children, this is okay
+ c->socket_event.closed = true;
+ poller_fd_reset (&c->socket_event);
+ client_cancel_timers (c);
+
+ LIST_UNLINK (ctx->clients, c);
+ ctx->n_clients--;
+ client_destroy (c);
+
+ irc_try_finish_quit (ctx);
+}
+
+static void
+client_close_link (struct client *c, const char *reason)
+{
+ // Let's just cut the connection, the client can try again later.
+ // We also want to avoid accidentally setting poller events before
+ // address resolution has finished.
+ if (!c->initialized)
+ {
+ client_kill (c, reason);
+ return;
+ }
+ if (!soft_assert (!c->closing_link))
+ return;
+
+ // We push an `ERROR' message to the write buffer and let the poller send
+ // it, with some arbitrary timeout. The `closing_link' state makes sure
+ // that a/ we ignore any successive messages, and b/ that the connection
+ // is killed after the write buffer is transferred and emptied.
+ client_send (c, "ERROR :Closing Link: %s[%s] (%s)",
+ c->nickname ? c->nickname : "*",
+ c->hostname /* TODO host IP? */, reason);
+ c->closing_link = true;
+
+ client_unregister (c, reason);
+ client_set_kill_timer (c);
+}
+
+static bool
+client_in_mask_list (const struct client *c, const struct strv *mask)
+{
+ char *client = xstrdup_printf ("%s!%s@%s",
+ c->nickname, c->username, c->hostname);
+ bool result = false;
+ for (size_t i = 0; i < mask->len; i++)
+ if (!irc_fnmatch (mask->vector[i], client))
+ {
+ result = true;
+ break;
+ }
+ free (client);
+ return result;
+}
+
+static char *
+client_get_ssl_cert_fingerprint (struct client *c)
+{
+ if (!c->ssl)
+ return NULL;
+
+ X509 *peer_cert = SSL_get_peer_certificate (c->ssl);
+ if (!peer_cert)
+ return NULL;
+
+ int cert_len = i2d_X509 (peer_cert, NULL);
+ if (cert_len < 0)
+ return NULL;
+
+ unsigned char cert[cert_len], *p = cert;
+ if (i2d_X509 (peer_cert, &p) < 0)
+ return NULL;
+
+ unsigned char hash[SHA_DIGEST_LENGTH];
+ SHA1 (cert, cert_len, hash);
+
+ struct str fingerprint = str_make ();
+ for (size_t i = 0; i < sizeof hash; i++)
+ str_append_printf (&fingerprint, "%02x", hash[i]);
+ return str_steal (&fingerprint);
+}
+
+// --- Timers ------------------------------------------------------------------
+
+static void
+client_cancel_timers (struct client *c)
+{
+ poller_timer_reset (&c->kill_timer);
+ poller_timer_reset (&c->timeout_timer);
+ poller_timer_reset (&c->ping_timer);
+ poller_timer_reset (&c->gni_timer);
+}
+
+static void
+client_set_timer (struct client *c,
+ struct poller_timer *timer, unsigned interval)
+{
+ client_cancel_timers (c);
+ poller_timer_set (timer, interval * 1000);
+}
+
+static void
+on_client_kill_timer (struct client *c)
+{
+ hard_assert (!c->initialized || c->closing_link);
+ client_kill (c, NULL);
+}
+
+static void
+client_set_kill_timer (struct client *c)
+{
+ client_set_timer (c, &c->kill_timer, c->ctx->ping_interval);
+}
+
+static void
+on_client_timeout_timer (struct client *c)
+{
+ char *reason = xstrdup_printf
+ ("Ping timeout: >%u seconds", c->ctx->ping_interval);
+ client_close_link (c, reason);
+ free (reason);
+}
+
+static void
+on_client_ping_timer (struct client *c)
+{
+ hard_assert (!c->closing_link);
+ client_send (c, "PING :%s", c->ctx->server_name);
+ client_set_timer (c, &c->timeout_timer, c->ctx->ping_interval);
+}
+
+static void
+client_set_ping_timer (struct client *c)
+{
+ client_set_timer (c, &c->ping_timer, c->ctx->ping_interval);
+}
+
+// --- IRC command handling ----------------------------------------------------
+
+static void
+irc_make_reply (struct client *c, int id, va_list ap, struct str *output)
+{
+ str_append_printf (output, ":%s %03d %s ",
+ c->ctx->server_name, id, c->nickname ? c->nickname : "*");
+ str_append_vprintf (output,
+ irc_get_text (c->ctx, id, g_default_replies[id]), ap);
+}
+
+// XXX: this way we cannot typecheck the arguments, so we must be careful
+static void
+irc_send_reply (struct client *c, int id, ...)
+{
+ struct str reply = str_make ();
+
+ va_list ap;
+ va_start (ap, id);
+ irc_make_reply (c, id, ap, &reply);
+ va_end (ap);
+
+ client_send_str (c, &reply);
+ str_free (&reply);
+}
+
+/// Send a space-separated list of words across as many replies as needed
+static void
+irc_send_reply_vector (struct client *c, int id, char **items, ...)
+{
+ struct str common = str_make ();
+
+ va_list ap;
+ va_start (ap, items);
+ irc_make_reply (c, id, ap, &common);
+ va_end (ap);
+
+ // We always send at least one message (there might be a client that
+ // expects us to send this message at least once)
+ do
+ {
+ struct str reply = str_make ();
+ str_append_str (&reply, &common);
+
+ // If not even a single item fits in the limit (which may happen,
+ // in theory) it just gets cropped. We could also skip it.
+ if (*items)
+ str_append (&reply, *items++);
+
+ // Append as many items as fits in a single message
+ while (*items
+ && reply.len + 1 + strlen (*items) <= IRC_MAX_MESSAGE_LENGTH)
+ str_append_printf (&reply, " %s", *items++);
+
+ client_send_str (c, &reply);
+ str_free (&reply);
+ }
+ while (*items);
+ str_free (&common);
+}
+
+#define RETURN_WITH_REPLY(c, ...) \
+ BLOCK_START \
+ irc_send_reply ((c), __VA_ARGS__); \
+ return; \
+ BLOCK_END
+
+static void
+irc_send_motd (struct client *c)
+{
+ struct server_context *ctx = c->ctx;
+ if (!ctx->motd.len)
+ RETURN_WITH_REPLY (c, IRC_ERR_NOMOTD);
+
+ irc_send_reply (c, IRC_RPL_MOTDSTART, ctx->server_name);
+ for (size_t i = 0; i < ctx->motd.len; i++)
+ irc_send_reply (c, IRC_RPL_MOTD, ctx->motd.vector[i]);
+ irc_send_reply (c, IRC_RPL_ENDOFMOTD);
+}
+
+static void
+irc_send_lusers (struct client *c)
+{
+ int n_users = 0, n_services = 0, n_opers = 0, n_unknown = 0;
+ for (struct client *link = c->ctx->clients; link; link = link->next)
+ {
+ if (link->registered)
+ n_users++;
+ else
+ n_unknown++;
+ if (link->mode & IRC_USER_MODE_OPERATOR)
+ n_opers++;
+ }
+
+ int n_channels = 0;
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ struct channel *chan;
+ while ((chan = str_map_iter_next (&iter)))
+ if (!(chan->modes & IRC_CHAN_MODE_SECRET)
+ || channel_get_user (chan, c))
+ n_channels++;
+
+ irc_send_reply (c, IRC_RPL_LUSERCLIENT,
+ n_users, n_services, 1 /* servers total */);
+ if (n_opers)
+ irc_send_reply (c, IRC_RPL_LUSEROP, n_opers);
+ if (n_unknown)
+ irc_send_reply (c, IRC_RPL_LUSERUNKNOWN, n_unknown);
+ if (n_channels)
+ irc_send_reply (c, IRC_RPL_LUSERCHANNELS, n_channels);
+ irc_send_reply (c, IRC_RPL_LUSERME,
+ n_users + n_services + n_unknown, 0 /* peer servers */);
+}
+
+static bool
+irc_is_this_me (struct server_context *ctx, const char *target)
+{
+ // Target servers can also be matched by their users
+ return !irc_fnmatch (target, ctx->server_name)
+ || str_map_find (&ctx->users, target);
+}
+
+static void
+irc_send_isupport (struct client *c)
+{
+ // Only # channels, +e supported, +I supported, unlimited arguments to MODE
+ irc_send_reply (c, IRC_RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"
+ " TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"
+ " NICKLEN=" XSTRINGIFY (IRC_MAX_NICKNAME)
+ " CHANNELLEN=" XSTRINGIFY (IRC_MAX_CHANNEL_NAME));
+}
+
+static void
+irc_try_finish_registration (struct client *c)
+{
+ struct server_context *ctx = c->ctx;
+ if (!c->nickname || !c->username || !c->realname)
+ return;
+ if (c->registered || c->cap_negotiating)
+ return;
+
+ c->registered = true;
+ irc_send_reply (c, IRC_RPL_WELCOME, c->nickname, c->username, c->hostname);
+
+ irc_send_reply (c, IRC_RPL_YOURHOST, ctx->server_name, PROGRAM_VERSION);
+ // The purpose of this message eludes me
+ irc_send_reply (c, IRC_RPL_CREATED, __DATE__);
+ irc_send_reply (c, IRC_RPL_MYINFO, ctx->server_name, PROGRAM_VERSION,
+ IRC_SUPPORTED_USER_MODES, IRC_SUPPORTED_CHAN_MODES);
+
+ irc_send_isupport (c);
+ irc_send_lusers (c);
+ irc_send_motd (c);
+
+ char *mode = client_get_mode (c);
+ if (*mode)
+ client_send (c, ":%s MODE %s :+%s", c->nickname, c->nickname, mode);
+ free (mode);
+
+ hard_assert (c->ssl_cert_fingerprint == NULL);
+ if ((c->ssl_cert_fingerprint = client_get_ssl_cert_fingerprint (c)))
+ client_send (c, ":%s NOTICE %s :"
+ "Your TLS client certificate fingerprint is %s",
+ ctx->server_name, c->nickname, c->ssl_cert_fingerprint);
+
+ str_map_set (&ctx->whowas, c->nickname, NULL);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// IRCv3 capability negotiation. See http://ircv3.org for details.
+
+struct irc_cap_args
+{
+ const char *subcommand; ///< The subcommand being processed
+ const char *full_params; ///< Whole parameter string
+ struct strv params; ///< Split parameters
+ const char *target; ///< Target parameter for replies
+};
+
+static struct
+{
+ unsigned flag; ///< Flag
+ const char *name; ///< Name of the capability
+}
+irc_cap_table[] =
+{
+ { IRC_CAP_MULTI_PREFIX, "multi-prefix" },
+ { IRC_CAP_INVITE_NOTIFY, "invite-notify" },
+ { IRC_CAP_ECHO_MESSAGE, "echo-message" },
+ { IRC_CAP_USERHOST_IN_NAMES, "userhost-in-names" },
+ { IRC_CAP_SERVER_TIME, "server-time" },
+};
+
+static void
+irc_handle_cap_ls (struct client *c, struct irc_cap_args *a)
+{
+ if (a->params.len == 1
+ && !xstrtoul (&c->cap_version, a->params.vector[0], 10))
+ irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
+ a->subcommand, "Ignoring invalid protocol version number");
+
+ c->cap_negotiating = true;
+ client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message"
+ " userhost-in-names server-time", c->ctx->server_name, a->target);
+}
+
+static void
+irc_handle_cap_list (struct client *c, struct irc_cap_args *a)
+{
+ struct strv caps = strv_make ();
+ for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
+ if (c->caps_enabled & irc_cap_table[i].flag)
+ strv_append (&caps, irc_cap_table[i].name);
+
+ char *caps_str = strv_join (&caps, " ");
+ strv_free (&caps);
+ client_send (c, ":%s CAP %s LIST :%s",
+ c->ctx->server_name, a->target, caps_str);
+ free (caps_str);
+}
+
+static unsigned
+irc_decode_capability (const char *name)
+{
+ for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
+ if (!strcmp (irc_cap_table[i].name, name))
+ return irc_cap_table[i].flag;
+ return 0;
+}
+
+static void
+irc_handle_cap_req (struct client *c, struct irc_cap_args *a)
+{
+ c->cap_negotiating = true;
+
+ unsigned new_caps = c->caps_enabled;
+ bool success = true;
+ for (size_t i = 0; i < a->params.len; i++)
+ {
+ bool removing = false;
+ const char *name = a->params.vector[i];
+ if (*name == '-')
+ {
+ removing = true;
+ name++;
+ }
+
+ unsigned cap;
+ if (!(cap = irc_decode_capability (name)))
+ success = false;
+ else if (removing)
+ new_caps &= ~cap;
+ else
+ new_caps |= cap;
+ }
+
+ if (success)
+ {
+ c->caps_enabled = new_caps;
+ client_send (c, ":%s CAP %s ACK :%s",
+ c->ctx->server_name, a->target, a->full_params);
+ }
+ else
+ client_send (c, ":%s CAP %s NAK :%s",
+ c->ctx->server_name, a->target, a->full_params);
+}
+
+static void
+irc_handle_cap_ack (struct client *c, struct irc_cap_args *a)
+{
+ if (a->params.len)
+ irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
+ a->subcommand, "No acknowledgable capabilities supported");
+}
+
+static void
+irc_handle_cap_end (struct client *c, struct irc_cap_args *a)
+{
+ (void) a;
+
+ c->cap_negotiating = false;
+ irc_try_finish_registration (c);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct irc_cap_command
+{
+ const char *name;
+ void (*handler) (struct client *, struct irc_cap_args *);
+};
+
+static void
+irc_register_cap_handlers (struct server_context *ctx)
+{
+ static const struct irc_cap_command cap_handlers[] =
+ {
+ { "LS", irc_handle_cap_ls },
+ { "LIST", irc_handle_cap_list },
+ { "REQ", irc_handle_cap_req },
+ { "ACK", irc_handle_cap_ack },
+ { "END", irc_handle_cap_end },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS (cap_handlers); i++)
+ {
+ const struct irc_cap_command *cmd = &cap_handlers[i];
+ str_map_set (&ctx->cap_handlers, cmd->name, (void *) cmd);
+ }
+}
+
+static void
+irc_handle_cap (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ struct irc_cap_args args;
+ args.target = c->nickname ? c->nickname : "*";
+ args.subcommand = msg->params.vector[0];
+ args.full_params = "";
+ args.params = strv_make ();
+
+ if (msg->params.len > 1)
+ {
+ args.full_params = msg->params.vector[1];
+ cstr_split (args.full_params, " ", true, &args.params);
+ }
+
+ struct irc_cap_command *cmd =
+ str_map_find (&c->ctx->cap_handlers, args.subcommand);
+ if (!cmd)
+ irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
+ args.subcommand, "Invalid CAP subcommand");
+ else
+ cmd->handler (c, &args);
+
+ strv_free (&args.params);
+}
+
+static void
+irc_handle_pass (const struct irc_message *msg, struct client *c)
+{
+ if (c->registered)
+ irc_send_reply (c, IRC_ERR_ALREADYREGISTERED);
+ else if (msg->params.len < 1)
+ irc_send_reply (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ // We have TLS client certificates for this purpose; ignoring
+}
+
+static void
+irc_handle_nick (const struct irc_message *msg, struct client *c)
+{
+ struct server_context *ctx = c->ctx;
+
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NONICKNAMEGIVEN);
+
+ const char *nickname = msg->params.vector[0];
+ if (irc_validate_nickname (nickname) != VALIDATION_OK)
+ RETURN_WITH_REPLY (c, IRC_ERR_ERRONEOUSNICKNAME, nickname);
+
+ struct client *client = str_map_find (&ctx->users, nickname);
+ if (client && client != c)
+ RETURN_WITH_REPLY (c, IRC_ERR_NICKNAMEINUSE, nickname);
+
+ // Nothing to do here, let's not annoy roommates
+ if (c->nickname && !strcmp (c->nickname, nickname))
+ return;
+
+ if (c->registered)
+ {
+ client_add_to_whowas (c);
+
+ char *message = xstrdup_printf (":%s!%s@%s NICK :%s",
+ c->nickname, c->username, c->hostname, nickname);
+ irc_send_to_roommates (c, message);
+ client_send (c, "%s", message);
+ free (message);
+ }
+
+ // Release the old nickname and allocate a new one
+ if (c->nickname)
+ str_map_set (&ctx->users, c->nickname, NULL);
+
+ cstr_set (&c->nickname, xstrdup (nickname));
+ str_map_set (&ctx->users, nickname, c);
+
+ irc_try_finish_registration (c);
+}
+
+static void
+irc_handle_user (const struct irc_message *msg, struct client *c)
+{
+ if (c->registered)
+ RETURN_WITH_REPLY (c, IRC_ERR_ALREADYREGISTERED);
+ if (msg->params.len < 4)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *username = msg->params.vector[0];
+ const char *mode = msg->params.vector[1];
+ const char *realname = msg->params.vector[3];
+
+ // Unfortunately the protocol doesn't give us any means of rejecting it
+ if (!irc_is_valid_user (username))
+ username = "xxx";
+
+ cstr_set (&c->username, xstrdup (username));
+ cstr_set (&c->realname, xstrdup (realname));
+ c->mode = 0;
+
+ unsigned long m;
+ if (xstrtoul (&m, mode, 10))
+ {
+ if (m & 4) c->mode |= IRC_USER_MODE_RX_WALLOPS;
+ if (m & 8) c->mode |= IRC_USER_MODE_INVISIBLE;
+ }
+
+ irc_try_finish_registration (c);
+}
+
+static void
+irc_handle_userhost (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ struct str reply = str_make ();
+ for (size_t i = 0; i < 5 && i < msg->params.len; i++)
+ {
+ const char *nick = msg->params.vector[i];
+ struct client *target = str_map_find (&c->ctx->users, nick);
+ if (!target)
+ continue;
+
+ if (i)
+ str_append_c (&reply, ' ');
+ str_append (&reply, nick);
+ if (target->mode & IRC_USER_MODE_OPERATOR)
+ str_append_c (&reply, '*');
+ str_append_printf (&reply, "=%c%s@%s",
+ target->away_message ? '-' : '+',
+ target->username, target->hostname);
+ }
+ irc_send_reply (c, IRC_RPL_USERHOST, reply.str);
+ str_free (&reply);
+}
+
+static void
+irc_handle_lusers (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
+ irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
+ else
+ irc_send_lusers (c);
+}
+
+static void
+irc_handle_motd (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+ else
+ irc_send_motd (c);
+}
+
+static void
+irc_handle_ping (const struct irc_message *msg, struct client *c)
+{
+ // XXX: the RFC is pretty incomprehensible about the exact usage
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
+ irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
+ else if (msg->params.len < 1)
+ irc_send_reply (c, IRC_ERR_NOORIGIN);
+ else
+ client_send (c, ":%s PONG :%s",
+ c->ctx->server_name, msg->params.vector[0]);
+}
+
+static void
+irc_handle_pong (const struct irc_message *msg, struct client *c)
+{
+ // We are the only server, so we don't have to care too much
+ if (msg->params.len < 1)
+ irc_send_reply (c, IRC_ERR_NOORIGIN);
+ else
+ // Set a new timer to send another PING
+ client_set_ping_timer (c);
+}
+
+static void
+irc_handle_quit (const struct irc_message *msg, struct client *c)
+{
+ char *reason = xstrdup_printf ("Quit: %s",
+ msg->params.len > 0 ? msg->params.vector[0] : c->nickname);
+ client_close_link (c, reason);
+ free (reason);
+}
+
+static void
+irc_handle_time (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+
+ char buf[32] = "";
+ time_t now = time (NULL);
+ struct tm tm;
+ strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&now, &tm));
+ irc_send_reply (c, IRC_RPL_TIME, c->ctx->server_name, buf);
+}
+
+static void
+irc_handle_version (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+
+ irc_send_reply (c, IRC_RPL_VERSION, PROGRAM_VERSION, g_debug_mode,
+ c->ctx->server_name, PROGRAM_NAME " " PROGRAM_VERSION);
+ irc_send_isupport (c);
+}
+
+static void
+irc_channel_multicast (struct channel *chan, const char *message,
+ struct client *except)
+{
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ if (iter->c != except)
+ client_send (iter->c, "%s", message);
+}
+
+static bool
+irc_modify_mode (unsigned *mask, unsigned mode, bool add)
+{
+ unsigned orig = *mask;
+ if (add)
+ *mask |= mode;
+ else
+ *mask &= ~mode;
+ return *mask != orig;
+}
+
+static void
+irc_update_user_mode (struct client *c, unsigned new_mode)
+{
+ unsigned old_mode = c->mode;
+ c->mode = new_mode;
+
+ unsigned added = new_mode & ~old_mode;
+ unsigned removed = old_mode & ~new_mode;
+
+ struct str diff = str_make ();
+ if (added)
+ {
+ str_append_c (&diff, '+');
+ client_mode_to_str (added, &diff);
+ }
+ if (removed)
+ {
+ str_append_c (&diff, '-');
+ client_mode_to_str (removed, &diff);
+ }
+
+ if (diff.len)
+ client_send (c, ":%s MODE %s :%s",
+ c->nickname, c->nickname, diff.str);
+ str_free (&diff);
+}
+
+static void
+irc_handle_user_mode_change (struct client *c, const char *mode_string)
+{
+ unsigned new_mode = c->mode;
+ bool adding = true;
+
+ while (*mode_string)
+ switch (*mode_string++)
+ {
+ case '+': adding = true; break;
+ case '-': adding = false; break;
+
+ case 'a':
+ // Ignore, the client should use AWAY
+ break;
+ case 'i':
+ irc_modify_mode (&new_mode, IRC_USER_MODE_INVISIBLE, adding);
+ break;
+ case 'w':
+ irc_modify_mode (&new_mode, IRC_USER_MODE_RX_WALLOPS, adding);
+ break;
+ case 'r':
+ // It's not possible to un-restrict yourself
+ if (adding)
+ new_mode |= IRC_USER_MODE_RESTRICTED;
+ break;
+ case 'o':
+ if (!adding)
+ new_mode &= ~IRC_USER_MODE_OPERATOR;
+ else if (c->ssl_cert_fingerprint
+ && str_map_find (&c->ctx->operators, c->ssl_cert_fingerprint))
+ new_mode |= IRC_USER_MODE_OPERATOR;
+ else
+ client_send (c, ":%s NOTICE %s :Either you're not using an TLS"
+ " client certificate, or the fingerprint doesn't match",
+ c->ctx->server_name, c->nickname);
+ break;
+ case 's':
+ irc_modify_mode (&new_mode, IRC_USER_MODE_RX_SERVER_NOTICES, adding);
+ break;
+ default:
+ RETURN_WITH_REPLY (c, IRC_ERR_UMODEUNKNOWNFLAG);
+ }
+ irc_update_user_mode (c, new_mode);
+}
+
+static void
+irc_send_channel_list (struct client *c, const char *channel_name,
+ const struct strv *list, int reply, int end_reply)
+{
+ for (size_t i = 0; i < list->len; i++)
+ irc_send_reply (c, reply, channel_name, list->vector[i]);
+ irc_send_reply (c, end_reply, channel_name);
+}
+
+static char *
+irc_check_expand_user_mask (const char *mask)
+{
+ struct str result = str_make ();
+ str_append (&result, mask);
+
+ // Make sure it is a complete mask
+ if (!strchr (result.str, '!'))
+ str_append (&result, "!*");
+ if (!strchr (result.str, '@'))
+ str_append (&result, "@*");
+
+ // And validate whatever the result is
+ if (!irc_is_valid_user_mask (result.str))
+ {
+ str_free (&result);
+ return NULL;
+ }
+ return str_steal (&result);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Channel MODE command handling. This is by far the worst command to implement
+// from the whole RFC; don't blame me if it doesn't work exactly as expected.
+
+struct mode_processor
+{
+ // Inputs to set after initialization:
+
+ char **params; ///< Mode string parameters
+
+ struct client *c; ///< Who does the changes
+ struct channel *channel; ///< The channel we're modifying
+ struct channel_user *user; ///< Presence of the client in the chan
+
+ // Internals:
+
+ bool adding; ///< Currently adding modes
+ char mode_char; ///< Currently processed mode char
+
+ struct str added; ///< Added modes
+ struct str removed; ///< Removed modes
+
+ struct strv added_params; ///< Params for added modes
+ struct strv removed_params; ///< Params for removed modes
+
+ struct str *output; ///< "added" or "removed"
+ struct strv *output_params; ///< Similarly for "*_params"
+};
+
+static struct mode_processor
+mode_processor_make (void)
+{
+ return (struct mode_processor)
+ {
+ .added = str_make (), .added_params = strv_make (),
+ .removed = str_make (), .removed_params = strv_make (),
+ };
+}
+
+static void
+mode_processor_free (struct mode_processor *self)
+{
+ str_free (&self->added);
+ str_free (&self->removed);
+
+ strv_free (&self->added_params);
+ strv_free (&self->removed_params);
+}
+
+static const char *
+mode_processor_next_param (struct mode_processor *self)
+{
+ if (!*self->params)
+ return NULL;
+ return *self->params++;
+}
+
+static bool
+mode_processor_check_operator (struct mode_processor *self)
+{
+ if ((self->user && (self->user->modes & IRC_CHAN_MODE_OPERATOR))
+ || (self->c->mode & IRC_USER_MODE_OPERATOR))
+ return true;
+
+ irc_send_reply (self->c, IRC_ERR_CHANOPRIVSNEEDED, self->channel->name);
+ return false;
+}
+
+static void
+mode_processor_do_user (struct mode_processor *self, int mode)
+{
+ const char *target = mode_processor_next_param (self);
+ if (!mode_processor_check_operator (self) || !target)
+ return;
+
+ struct client *client;
+ struct channel_user *target_user;
+ if (!(client = str_map_find (&self->c->ctx->users, target)))
+ irc_send_reply (self->c, IRC_ERR_NOSUCHNICK, target);
+ else if (!(target_user = channel_get_user (self->channel, client)))
+ irc_send_reply (self->c, IRC_ERR_USERNOTINCHANNEL,
+ target, self->channel->name);
+ else if (irc_modify_mode (&target_user->modes, mode, self->adding))
+ {
+ str_append_c (self->output, self->mode_char);
+ strv_append (self->output_params, client->nickname);
+ }
+}
+
+static bool
+mode_processor_do_chan (struct mode_processor *self, int mode)
+{
+ if (!mode_processor_check_operator (self)
+ || !irc_modify_mode (&self->channel->modes, mode, self->adding))
+ return false;
+
+ str_append_c (self->output, self->mode_char);
+ return true;
+}
+
+static void
+mode_processor_do_chan_remove
+ (struct mode_processor *self, char mode_char, int mode)
+{
+ if (self->adding
+ && irc_modify_mode (&self->channel->modes, mode, false))
+ str_append_c (&self->removed, mode_char);
+}
+
+static void
+mode_processor_do_list (struct mode_processor *self,
+ struct strv *list, int list_msg, int end_msg)
+{
+ const char *target = mode_processor_next_param (self);
+ if (!target)
+ {
+ if (self->adding)
+ irc_send_channel_list (self->c, self->channel->name,
+ list, list_msg, end_msg);
+ return;
+ }
+
+ if (!mode_processor_check_operator (self))
+ return;
+
+ char *mask = irc_check_expand_user_mask (target);
+ if (!mask)
+ return;
+
+ size_t i;
+ for (i = 0; i < list->len; i++)
+ if (!irc_strcmp (list->vector[i], mask))
+ break;
+
+ bool found = i != list->len;
+ if (found != self->adding)
+ {
+ if (self->adding)
+ strv_append (list, mask);
+ else
+ strv_remove (list, i);
+
+ str_append_c (self->output, self->mode_char);
+ strv_append (self->output_params, mask);
+ }
+ free (mask);
+}
+
+static void
+mode_processor_do_key (struct mode_processor *self)
+{
+ const char *target = mode_processor_next_param (self);
+ if (!mode_processor_check_operator (self) || !target)
+ return;
+
+ if (!self->adding)
+ {
+ if (!self->channel->key || irc_strcmp (target, self->channel->key))
+ return;
+
+ str_append_c (&self->removed, self->mode_char);
+ strv_append (&self->removed_params, self->channel->key);
+ cstr_set (&self->channel->key, NULL);
+ }
+ else if (!irc_is_valid_key (target))
+ // TODO: we should notify the user somehow
+ return;
+ else if (self->channel->key)
+ irc_send_reply (self->c, IRC_ERR_KEYSET, self->channel->name);
+ else
+ {
+ self->channel->key = xstrdup (target);
+ str_append_c (&self->added, self->mode_char);
+ strv_append (&self->added_params, self->channel->key);
+ }
+}
+
+static void
+mode_processor_do_limit (struct mode_processor *self)
+{
+ if (!mode_processor_check_operator (self))
+ return;
+
+ const char *target;
+ if (!self->adding)
+ {
+ if (self->channel->user_limit == -1)
+ return;
+
+ self->channel->user_limit = -1;
+ str_append_c (&self->removed, self->mode_char);
+ }
+ else if ((target = mode_processor_next_param (self)))
+ {
+ unsigned long x;
+ if (xstrtoul (&x, target, 10) && x > 0 && x <= LONG_MAX)
+ {
+ self->channel->user_limit = x;
+ str_append_c (&self->added, self->mode_char);
+ strv_append (&self->added_params, target);
+ }
+ }
+}
+
+static bool
+mode_processor_step (struct mode_processor *self, char mode_char)
+{
+ switch ((self->mode_char = mode_char))
+ {
+ case '+':
+ self->adding = true;
+ self->output = &self->added;
+ self->output_params = &self->added_params;
+ break;
+ case '-':
+ self->adding = false;
+ self->output = &self->removed;
+ self->output_params = &self->removed_params;
+ break;
+
+#define USER(mode) mode_processor_do_user (self, (mode))
+#define CHAN(mode) mode_processor_do_chan (self, (mode))
+
+ case 'o': USER (IRC_CHAN_MODE_OPERATOR); break;
+ case 'v': USER (IRC_CHAN_MODE_VOICE); break;
+
+ case 'i': CHAN (IRC_CHAN_MODE_INVITE_ONLY); break;
+ case 'm': CHAN (IRC_CHAN_MODE_MODERATED); break;
+ case 'n': CHAN (IRC_CHAN_MODE_NO_OUTSIDE_MSGS); break;
+ case 'q': CHAN (IRC_CHAN_MODE_QUIET); break;
+ case 't': CHAN (IRC_CHAN_MODE_PROTECTED_TOPIC); break;
+
+ case 'p':
+ if (CHAN (IRC_CHAN_MODE_PRIVATE))
+ mode_processor_do_chan_remove (self, 's', IRC_CHAN_MODE_SECRET);
+ break;
+ case 's':
+ if (CHAN (IRC_CHAN_MODE_SECRET))
+ mode_processor_do_chan_remove (self, 'p', IRC_CHAN_MODE_PRIVATE);
+ break;
+
+#undef USER
+#undef CHAN
+
+ case 'b':
+ mode_processor_do_list (self, &self->channel->ban_list,
+ IRC_RPL_BANLIST, IRC_RPL_ENDOFBANLIST);
+ break;
+ case 'e':
+ mode_processor_do_list (self, &self->channel->exception_list,
+ IRC_RPL_EXCEPTLIST, IRC_RPL_ENDOFEXCEPTLIST);
+ break;
+ case 'I':
+ mode_processor_do_list (self, &self->channel->invite_list,
+ IRC_RPL_INVITELIST, IRC_RPL_ENDOFINVITELIST);
+ break;
+
+ case 'k':
+ mode_processor_do_key (self);
+ break;
+ case 'l':
+ mode_processor_do_limit (self);
+ break;
+
+ default:
+ // It's not safe to continue, results could be undesired
+ irc_send_reply (self->c, IRC_ERR_UNKNOWNMODE,
+ mode_char, self->channel->name);
+ return false;
+ }
+ return true;
+}
+
+static void
+irc_handle_chan_mode_change
+ (struct client *c, struct channel *chan, char *params[])
+{
+ struct mode_processor p = mode_processor_make ();
+ p.params = params;
+ p.channel = chan;
+ p.c = c;
+ p.user = channel_get_user (chan, c);
+
+ const char *mode_string;
+ while ((mode_string = mode_processor_next_param (&p)))
+ {
+ mode_processor_step (&p, '+');
+ while (*mode_string)
+ if (!mode_processor_step (&p, *mode_string++))
+ goto done_processing;
+ }
+
+ // TODO: limit to three changes with parameter per command
+done_processing:
+ if (p.added.len || p.removed.len)
+ {
+ struct str message = str_make ();
+ str_append_printf (&message, ":%s!%s@%s MODE %s ",
+ p.c->nickname, p.c->username, p.c->hostname,
+ p.channel->name);
+ if (p.added.len)
+ str_append_printf (&message, "+%s", p.added.str);
+ if (p.removed.len)
+ str_append_printf (&message, "-%s", p.removed.str);
+ for (size_t i = 0; i < p.added_params.len; i++)
+ str_append_printf (&message, " %s", p.added_params.vector[i]);
+ for (size_t i = 0; i < p.removed_params.len; i++)
+ str_append_printf (&message, " %s", p.removed_params.vector[i]);
+ irc_channel_multicast (p.channel, message.str, NULL);
+ str_free (&message);
+ }
+ mode_processor_free (&p);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+irc_handle_mode (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *target = msg->params.vector[0];
+ struct client *client = str_map_find (&c->ctx->users, target);
+ struct channel *chan = str_map_find (&c->ctx->channels, target);
+
+ if (client)
+ {
+ if (irc_strcmp (target, c->nickname))
+ RETURN_WITH_REPLY (c, IRC_ERR_USERSDONTMATCH);
+
+ if (msg->params.len < 2)
+ {
+ char *mode = client_get_mode (client);
+ irc_send_reply (c, IRC_RPL_UMODEIS, mode);
+ free (mode);
+ }
+ else
+ irc_handle_user_mode_change (c, msg->params.vector[1]);
+ }
+ else if (chan)
+ {
+ if (msg->params.len < 2)
+ {
+ char *mode = channel_get_mode (chan, channel_get_user (chan, c));
+ irc_send_reply (c, IRC_RPL_CHANNELMODEIS, target, mode);
+ irc_send_reply (c, IRC_RPL_CREATIONTIME,
+ target, (long long) chan->created);
+ free (mode);
+ }
+ else
+ irc_handle_chan_mode_change (c, chan, &msg->params.vector[1]);
+ }
+ else
+ irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
+}
+
+static void
+irc_handle_user_message (const struct irc_message *msg, struct client *c,
+ const char *command, bool allow_away_reply)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NORECIPIENT, msg->command);
+ if (msg->params.len < 2 || !*msg->params.vector[1])
+ RETURN_WITH_REPLY (c, IRC_ERR_NOTEXTTOSEND);
+
+ const char *target = msg->params.vector[0];
+ const char *text = msg->params.vector[1];
+ struct client *client = str_map_find (&c->ctx->users, target);
+ if (client)
+ {
+ client_send (client, ":%s!%s@%s %s %s :%s",
+ c->nickname, c->username, c->hostname, command, target, text);
+ if (allow_away_reply && client->away_message)
+ irc_send_reply (c, IRC_RPL_AWAY, target, client->away_message);
+
+ // Acknowledging a message from the client to itself would be silly
+ if (client != c && (c->caps_enabled & IRC_CAP_ECHO_MESSAGE))
+ client_send (c, ":%s!%s@%s %s %s :%s",
+ c->nickname, c->username, c->hostname, command, target, text);
+ return;
+ }
+
+ struct channel *chan = str_map_find (&c->ctx->channels, target);
+ if (chan)
+ {
+ struct channel_user *user = channel_get_user (chan, c);
+ if ((chan->modes & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) && !user)
+ RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
+ if ((chan->modes & IRC_CHAN_MODE_MODERATED) && (!user ||
+ !(user->modes & (IRC_CHAN_MODE_VOICE | IRC_CHAN_MODE_OPERATOR))))
+ RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
+ if (client_in_mask_list (c, &chan->ban_list)
+ && !client_in_mask_list (c, &chan->exception_list))
+ RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
+
+ char *message = xstrdup_printf (":%s!%s@%s %s %s :%s",
+ c->nickname, c->username, c->hostname, command, target, text);
+ irc_channel_multicast (chan, message,
+ (c->caps_enabled & IRC_CAP_ECHO_MESSAGE) ? NULL : c);
+ free (message);
+ return;
+ }
+
+ irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
+}
+
+static void
+irc_handle_privmsg (const struct irc_message *msg, struct client *c)
+{
+ irc_handle_user_message (msg, c, "PRIVMSG", true);
+ // Let's not care too much about success or failure
+ c->last_active = time (NULL);
+}
+
+static void
+irc_handle_notice (const struct irc_message *msg, struct client *c)
+{
+ irc_handle_user_message (msg, c, "NOTICE", false);
+}
+
+static void
+irc_send_rpl_list (struct client *c, const struct channel *chan)
+{
+ int visible = 0;
+ for (struct channel_user *user = chan->users;
+ user; user = user->next)
+ // XXX: maybe we should skip IRC_USER_MODE_INVISIBLE
+ visible++;
+
+ irc_send_reply (c, IRC_RPL_LIST, chan->name, visible, chan->topic);
+}
+
+static void
+irc_handle_list (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
+
+ struct channel *chan;
+ if (msg->params.len == 0)
+ {
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ while ((chan = str_map_iter_next (&iter)))
+ if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
+ || channel_get_user (chan, c))
+ irc_send_rpl_list (c, chan);
+ }
+ else
+ {
+ struct strv channels = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &channels);
+ for (size_t i = 0; i < channels.len; i++)
+ if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
+ && (!(chan->modes & IRC_CHAN_MODE_SECRET)
+ || channel_get_user (chan, c)))
+ irc_send_rpl_list (c, chan);
+ strv_free (&channels);
+ }
+ irc_send_reply (c, IRC_RPL_LISTEND);
+}
+
+static void
+irc_append_prefixes (struct client *c, struct channel_user *user,
+ struct str *output)
+{
+ struct str prefixes = str_make ();
+ if (user->modes & IRC_CHAN_MODE_OPERATOR) str_append_c (&prefixes, '@');
+ if (user->modes & IRC_CHAN_MODE_VOICE) str_append_c (&prefixes, '+');
+
+ if (prefixes.len)
+ {
+ if (c->caps_enabled & IRC_CAP_MULTI_PREFIX)
+ str_append (output, prefixes.str);
+ else
+ str_append_c (output, prefixes.str[0]);
+ }
+ str_free (&prefixes);
+}
+
+static char *
+irc_make_rpl_namreply_item
+ (struct client *c, struct client *target, struct channel_user *user)
+{
+ struct str result = str_make ();
+
+ if (user)
+ irc_append_prefixes (c, user, &result);
+
+ str_append (&result, target->nickname);
+ if (c->caps_enabled & IRC_CAP_USERHOST_IN_NAMES)
+ str_append_printf (&result,
+ "!%s@%s", target->username, target->hostname);
+ return str_steal (&result);
+}
+
+static void
+irc_send_rpl_namreply (struct client *c, const struct channel *chan,
+ struct str_map *used_nicks)
+{
+ char type = '=';
+ if (chan->modes & IRC_CHAN_MODE_SECRET)
+ type = '@';
+ else if (chan->modes & IRC_CHAN_MODE_PRIVATE)
+ type = '*';
+
+ bool on_channel = channel_get_user (chan, c);
+ struct strv nicks = strv_make ();
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ {
+ if (!on_channel && (iter->c->mode & IRC_USER_MODE_INVISIBLE))
+ continue;
+ if (used_nicks)
+ str_map_set (used_nicks, iter->c->nickname, (void *) 1);
+ strv_append_owned (&nicks,
+ irc_make_rpl_namreply_item (c, iter->c, iter));
+ }
+
+ irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
+ nicks.vector, type, chan->name, "");
+ strv_free (&nicks);
+}
+
+static void
+irc_send_disassociated_names (struct client *c, struct str_map *used)
+{
+ struct strv nicks = strv_make ();
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
+ struct client *target;
+ while ((target = str_map_iter_next (&iter)))
+ {
+ if ((target->mode & IRC_USER_MODE_INVISIBLE)
+ || str_map_find (used, target->nickname))
+ continue;
+ strv_append_owned (&nicks,
+ irc_make_rpl_namreply_item (c, target, NULL));
+ }
+
+ if (nicks.len)
+ irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
+ nicks.vector, '*', "*", "");
+ strv_free (&nicks);
+}
+
+static void
+irc_handle_names (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
+
+ struct channel *chan;
+ if (msg->params.len == 0)
+ {
+ struct str_map used = str_map_make (NULL);
+ used.key_xfrm = irc_strxfrm;
+
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ while ((chan = str_map_iter_next (&iter)))
+ if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
+ || channel_get_user (chan, c))
+ irc_send_rpl_namreply (c, chan, &used);
+
+ // Also send all visible users we haven't listed yet
+ irc_send_disassociated_names (c, &used);
+ str_map_free (&used);
+
+ irc_send_reply (c, IRC_RPL_ENDOFNAMES, "*");
+ }
+ else
+ {
+ struct strv channels = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &channels);
+ for (size_t i = 0; i < channels.len; i++)
+ if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
+ && (!(chan->modes & IRC_CHAN_MODE_SECRET)
+ || channel_get_user (chan, c)))
+ {
+ irc_send_rpl_namreply (c, chan, NULL);
+ irc_send_reply (c, IRC_RPL_ENDOFNAMES, channels.vector[i]);
+ }
+ strv_free (&channels);
+ }
+}
+
+static void
+irc_send_rpl_whoreply (struct client *c, const struct channel *chan,
+ const struct client *target)
+{
+ struct str chars = str_make ();
+ str_append_c (&chars, target->away_message ? 'G' : 'H');
+ if (target->mode & IRC_USER_MODE_OPERATOR)
+ str_append_c (&chars, '*');
+
+ struct channel_user *user;
+ if (chan && (user = channel_get_user (chan, target)))
+ irc_append_prefixes (c, user, &chars);
+
+ irc_send_reply (c, IRC_RPL_WHOREPLY, chan ? chan->name : "*",
+ target->username, target->hostname, target->ctx->server_name,
+ target->nickname, chars.str, 0 /* hop count */, target->realname);
+ str_free (&chars);
+}
+
+static void
+irc_match_send_rpl_whoreply (struct client *c, struct client *target,
+ const char *mask)
+{
+ bool is_roommate = false;
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ struct channel *chan;
+ while ((chan = str_map_iter_next (&iter)))
+ if (channel_get_user (chan, target) && channel_get_user (chan, c))
+ {
+ is_roommate = true;
+ break;
+ }
+ if ((target->mode & IRC_USER_MODE_INVISIBLE) && !is_roommate)
+ return;
+
+ if (irc_fnmatch (mask, target->hostname)
+ && irc_fnmatch (mask, target->nickname)
+ && irc_fnmatch (mask, target->realname)
+ && irc_fnmatch (mask, c->ctx->server_name))
+ return;
+
+ // Try to find a channel they're on that's visible to us
+ struct channel *user_chan = NULL;
+ iter = str_map_iter_make (&c->ctx->channels);
+ while ((chan = str_map_iter_next (&iter)))
+ if (channel_get_user (chan, target)
+ && (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
+ || channel_get_user (chan, c)))
+ {
+ user_chan = chan;
+ break;
+ }
+ irc_send_rpl_whoreply (c, user_chan, target);
+}
+
+static void
+irc_handle_who (const struct irc_message *msg, struct client *c)
+{
+ bool only_ops = msg->params.len > 1 && !strcmp (msg->params.vector[1], "o");
+
+ const char *shown_mask = msg->params.vector[0], *used_mask;
+ if (!shown_mask)
+ used_mask = shown_mask = "*";
+ else if (!strcmp (shown_mask, "0"))
+ used_mask = "*";
+ else
+ used_mask = shown_mask;
+
+ struct channel *chan;
+ if ((chan = str_map_find (&c->ctx->channels, used_mask)))
+ {
+ bool on_chan = !!channel_get_user (chan, c);
+ if (on_chan || !(chan->modes & IRC_CHAN_MODE_SECRET))
+ for (struct channel_user *iter = chan->users;
+ iter; iter = iter->next)
+ {
+ if ((on_chan || !(iter->c->mode & IRC_USER_MODE_INVISIBLE))
+ && (!only_ops || (iter->c->mode & IRC_USER_MODE_OPERATOR)))
+ irc_send_rpl_whoreply (c, chan, iter->c);
+ }
+ }
+ else
+ {
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
+ struct client *target;
+ while ((target = str_map_iter_next (&iter)))
+ if (!only_ops || (target->mode & IRC_USER_MODE_OPERATOR))
+ irc_match_send_rpl_whoreply (c, target, used_mask);
+ }
+ irc_send_reply (c, IRC_RPL_ENDOFWHO, shown_mask);
+}
+
+static void
+irc_send_whois_reply (struct client *c, const struct client *target)
+{
+ const char *nick = target->nickname;
+ irc_send_reply (c, IRC_RPL_WHOISUSER, nick,
+ target->username, target->hostname, target->realname);
+ irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, target->ctx->server_name,
+ str_map_find (&c->ctx->config, "server_info"));
+ if (target->mode & IRC_USER_MODE_OPERATOR)
+ irc_send_reply (c, IRC_RPL_WHOISOPERATOR, nick);
+ irc_send_reply (c, IRC_RPL_WHOISIDLE, nick,
+ (int) (time (NULL) - target->last_active));
+ if (target->away_message)
+ irc_send_reply (c, IRC_RPL_AWAY, nick, target->away_message);
+
+ struct strv channels = strv_make ();
+
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
+ struct channel *chan;
+ struct channel_user *channel_user;
+ while ((chan = str_map_iter_next (&iter)))
+ if ((channel_user = channel_get_user (chan, target))
+ && (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
+ || channel_get_user (chan, c)))
+ {
+ struct str item = str_make ();
+ if (channel_user->modes & IRC_CHAN_MODE_OPERATOR)
+ str_append_c (&item, '@');
+ else if (channel_user->modes & IRC_CHAN_MODE_VOICE)
+ str_append_c (&item, '+');
+ str_append (&item, chan->name);
+ strv_append_owned (&channels, str_steal (&item));
+ }
+
+ irc_send_reply_vector (c, IRC_RPL_WHOISCHANNELS, channels.vector, nick, "");
+ strv_free (&channels);
+
+ irc_send_reply (c, IRC_RPL_ENDOFWHOIS, nick);
+}
+
+static void
+irc_handle_whois (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+
+ struct strv masks = strv_make ();
+ const char *masks_str = msg->params.vector[msg->params.len > 1];
+ cstr_split (masks_str, ",", true, &masks);
+ for (size_t i = 0; i < masks.len; i++)
+ {
+ const char *mask = masks.vector[i];
+ struct client *target;
+ if (!strpbrk (mask, "*?"))
+ {
+ if (!(target = str_map_find (&c->ctx->users, mask)))
+ irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
+ else
+ irc_send_whois_reply (c, target);
+ }
+ else
+ {
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
+ bool found = false;
+ while ((target = str_map_iter_next (&iter)))
+ if (!irc_fnmatch (mask, target->nickname))
+ {
+ irc_send_whois_reply (c, target);
+ found = true;
+ }
+ if (!found)
+ irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
+ }
+ }
+ strv_free (&masks);
+}
+
+static void
+irc_handle_whowas (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+ if (msg->params.len > 2 && !irc_is_this_me (c->ctx, msg->params.vector[2]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[2]);
+ // The "count" parameter is ignored, we only store one entry for a nick
+
+ struct strv nicks = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &nicks);
+
+ for (size_t i = 0; i < nicks.len; i++)
+ {
+ const char *nick = nicks.vector[i];
+ struct whowas_info *info = str_map_find (&c->ctx->whowas, nick);
+ if (!info)
+ irc_send_reply (c, IRC_ERR_WASNOSUCHNICK, nick);
+ else
+ {
+ irc_send_reply (c, IRC_RPL_WHOWASUSER, nick,
+ info->username, info->hostname, info->realname);
+ irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, c->ctx->server_name,
+ str_map_find (&c->ctx->config, "server_info"));
+ }
+ irc_send_reply (c, IRC_RPL_ENDOFWHOWAS, nick);
+ }
+ strv_free (&nicks);
+}
+
+static void
+irc_send_rpl_topic (struct client *c, struct channel *chan)
+{
+ if (!*chan->topic)
+ irc_send_reply (c, IRC_RPL_NOTOPIC, chan->name);
+ else
+ {
+ irc_send_reply (c, IRC_RPL_TOPIC, chan->name, chan->topic);
+ irc_send_reply (c, IRC_RPL_TOPICWHOTIME,
+ chan->name, chan->topic_who, (long long) chan->topic_time);
+ }
+}
+
+static void
+irc_handle_topic (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *target = msg->params.vector[0];
+ struct channel *chan = str_map_find (&c->ctx->channels, target);
+ if (!chan)
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, target);
+
+ if (msg->params.len < 2)
+ {
+ irc_send_rpl_topic (c, chan);
+ return;
+ }
+
+ struct channel_user *user = channel_get_user (chan, c);
+ if (!user)
+ RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, target);
+
+ if ((chan->modes & IRC_CHAN_MODE_PROTECTED_TOPIC)
+ && !(user->modes & IRC_CHAN_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, target);
+
+ cstr_set (&chan->topic, xstrdup (msg->params.vector[1]));
+ cstr_set (&chan->topic_who, xstrdup_printf
+ ("%s!%s@%s", c->nickname, c->username, c->hostname));
+ chan->topic_time = time (NULL);
+
+ char *message = xstrdup_printf (":%s!%s@%s TOPIC %s :%s",
+ c->nickname, c->username, c->hostname, target, chan->topic);
+ irc_channel_multicast (chan, message, NULL);
+ free (message);
+}
+
+static void
+irc_try_part (struct client *c, const char *channel_name, const char *reason)
+{
+ if (!reason)
+ reason = c->nickname;
+
+ struct channel *chan;
+ if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
+
+ struct channel_user *user;
+ if (!(user = channel_get_user (chan, c)))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
+
+ char *message = xstrdup_printf (":%s!%s@%s PART %s :%s",
+ c->nickname, c->username, c->hostname, channel_name, reason);
+ if (!(chan->modes & IRC_CHAN_MODE_QUIET))
+ irc_channel_multicast (chan, message, NULL);
+ else
+ client_send (c, "%s", message);
+ free (message);
+
+ channel_remove_user (chan, user);
+ irc_channel_destroy_if_empty (c->ctx, chan);
+}
+
+static void
+irc_part_all_channels (struct client *c)
+{
+ // We have to be careful here, the channel might get destroyed
+ struct str_map_unset_iter iter =
+ str_map_unset_iter_make (&c->ctx->channels);
+
+ struct channel *chan;
+ while ((chan = str_map_unset_iter_next (&iter)))
+ if (channel_get_user (chan, c))
+ irc_try_part (c, chan->name, NULL);
+ str_map_unset_iter_free (&iter);
+}
+
+static void
+irc_handle_part (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *reason = msg->params.len > 1 ? msg->params.vector[1] : NULL;
+ struct strv channels = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &channels);
+ for (size_t i = 0; i < channels.len; i++)
+ irc_try_part (c, channels.vector[i], reason);
+ strv_free (&channels);
+}
+
+static void
+irc_try_kick (struct client *c, const char *channel_name, const char *nick,
+ const char *reason)
+{
+ struct channel *chan;
+ if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
+
+ struct channel_user *user;
+ if (!(user = channel_get_user (chan, c)))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
+ if (!(user->modes & IRC_CHAN_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
+
+ struct client *client;
+ if (!(client = str_map_find (&c->ctx->users, nick))
+ || !(user = channel_get_user (chan, client)))
+ RETURN_WITH_REPLY (c, IRC_ERR_USERNOTINCHANNEL, nick, channel_name);
+
+ char *message = xstrdup_printf (":%s!%s@%s KICK %s %s :%s",
+ c->nickname, c->username, c->hostname, channel_name, nick, reason);
+ if (!(chan->modes & IRC_CHAN_MODE_QUIET))
+ irc_channel_multicast (chan, message, NULL);
+ else
+ client_send (c, "%s", message);
+ free (message);
+
+ channel_remove_user (chan, user);
+ irc_channel_destroy_if_empty (c->ctx, chan);
+}
+
+static void
+irc_handle_kick (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 2)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *reason = c->nickname;
+ if (msg->params.len > 2)
+ reason = msg->params.vector[2];
+
+ struct strv channels = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &channels);
+ struct strv users = strv_make ();
+ cstr_split (msg->params.vector[1], ",", true, &users);
+
+ if (channels.len == 1)
+ for (size_t i = 0; i < users.len; i++)
+ irc_try_kick (c, channels.vector[0], users.vector[i], reason);
+ else
+ for (size_t i = 0; i < channels.len && i < users.len; i++)
+ irc_try_kick (c, channels.vector[i], users.vector[i], reason);
+
+ strv_free (&channels);
+ strv_free (&users);
+}
+
+static void
+irc_send_invite_notifications
+ (struct channel *chan, struct client *c, struct client *target)
+{
+ for (struct channel_user *iter = chan->users; iter; iter = iter->next)
+ if (iter->c != target && iter->c->caps_enabled & IRC_CAP_INVITE_NOTIFY)
+ client_send (iter->c, ":%s!%s@%s INVITE %s %s",
+ c->nickname, c->username, c->hostname,
+ target->nickname, chan->name);
+}
+
+static void
+irc_handle_invite (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 2)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ const char *target = msg->params.vector[0];
+ const char *channel_name = msg->params.vector[1];
+
+ struct client *client = str_map_find (&c->ctx->users, target);
+ if (!client)
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, target);
+
+ struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
+ if (chan)
+ {
+ struct channel_user *inviting_user;
+ if (!(inviting_user = channel_get_user (chan, c)))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
+ if (channel_get_user (chan, client))
+ RETURN_WITH_REPLY (c, IRC_ERR_USERONCHANNEL, target, channel_name);
+
+ if ((inviting_user->modes & IRC_CHAN_MODE_OPERATOR))
+ str_map_set (&client->invites, channel_name, (void *) 1);
+ else if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY))
+ RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
+
+ // It's not specified when and how we should send out invite-notify
+ if (chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
+ irc_send_invite_notifications (chan, c, client);
+ }
+
+ client_send (client, ":%s!%s@%s INVITE %s %s",
+ c->nickname, c->username, c->hostname, client->nickname, channel_name);
+ if (client->away_message)
+ irc_send_reply (c, IRC_RPL_AWAY,
+ client->nickname, client->away_message);
+ irc_send_reply (c, IRC_RPL_INVITING, client->nickname, channel_name);
+}
+
+static void
+irc_try_join (struct client *c, const char *channel_name, const char *key)
+{
+ struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
+ unsigned user_mode = 0;
+ if (!chan)
+ {
+ if (irc_validate_channel_name (channel_name) != VALIDATION_OK)
+ RETURN_WITH_REPLY (c, IRC_ERR_BADCHANMASK, channel_name);
+ chan = irc_channel_create (c->ctx, channel_name);
+ user_mode = IRC_CHAN_MODE_OPERATOR;
+ }
+ else if (channel_get_user (chan, c))
+ return;
+
+ bool invited_by_chanop = !!str_map_find (&c->invites, channel_name);
+ if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
+ && !client_in_mask_list (c, &chan->invite_list)
+ && !invited_by_chanop)
+ RETURN_WITH_REPLY (c, IRC_ERR_INVITEONLYCHAN, channel_name);
+ if (chan->key && (!key || strcmp (key, chan->key)))
+ RETURN_WITH_REPLY (c, IRC_ERR_BADCHANNELKEY, channel_name);
+ if (chan->user_limit != -1
+ && channel_user_count (chan) >= (size_t) chan->user_limit)
+ RETURN_WITH_REPLY (c, IRC_ERR_CHANNELISFULL, channel_name);
+ if (client_in_mask_list (c, &chan->ban_list)
+ && !client_in_mask_list (c, &chan->exception_list)
+ && !invited_by_chanop)
+ RETURN_WITH_REPLY (c, IRC_ERR_BANNEDFROMCHAN, channel_name);
+
+ // Destroy any invitation as there's no other way to get rid of it
+ str_map_set (&c->invites, channel_name, NULL);
+
+ channel_add_user (chan, c)->modes = user_mode;
+
+ char *message = xstrdup_printf (":%s!%s@%s JOIN %s",
+ c->nickname, c->username, c->hostname, channel_name);
+ if (!(chan->modes & IRC_CHAN_MODE_QUIET))
+ irc_channel_multicast (chan, message, NULL);
+ else
+ client_send (c, "%s", message);
+ free (message);
+
+ irc_send_rpl_topic (c, chan);
+ irc_send_rpl_namreply (c, chan, NULL);
+ irc_send_reply (c, IRC_RPL_ENDOFNAMES, chan->name);
+}
+
+static void
+irc_handle_join (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ if (!strcmp (msg->params.vector[0], "0"))
+ {
+ irc_part_all_channels (c);
+ return;
+ }
+
+ struct strv channels = strv_make ();
+ cstr_split (msg->params.vector[0], ",", true, &channels);
+ struct strv keys = strv_make ();
+ if (msg->params.len > 1)
+ cstr_split (msg->params.vector[1], ",", true, &keys);
+
+ for (size_t i = 0; i < channels.len; i++)
+ irc_try_join (c, channels.vector[i],
+ i < keys.len ? keys.vector[i] : NULL);
+
+ strv_free (&channels);
+ strv_free (&keys);
+}
+
+static void
+irc_handle_summon (const struct irc_message *msg, struct client *c)
+{
+ (void) msg;
+ irc_send_reply (c, IRC_ERR_SUMMONDISABLED);
+}
+
+static void
+irc_handle_users (const struct irc_message *msg, struct client *c)
+{
+ (void) msg;
+ irc_send_reply (c, IRC_ERR_USERSDISABLED);
+}
+
+static void
+irc_handle_away (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ {
+ cstr_set (&c->away_message, NULL);
+ irc_send_reply (c, IRC_RPL_UNAWAY);
+ }
+ else
+ {
+ cstr_set (&c->away_message, xstrdup (msg->params.vector[0]));
+ irc_send_reply (c, IRC_RPL_NOWAWAY);
+ }
+}
+
+static void
+irc_handle_ison (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+
+ struct str result = str_make ();
+ const char *nick;
+ if (str_map_find (&c->ctx->users, (nick = msg->params.vector[0])))
+ str_append (&result, nick);
+ for (size_t i = 1; i < msg->params.len; i++)
+ if (str_map_find (&c->ctx->users, (nick = msg->params.vector[i])))
+ str_append_printf (&result, " %s", nick);
+
+ irc_send_reply (c, IRC_RPL_ISON, result.str);
+ str_free (&result);
+}
+
+static void
+irc_handle_admin (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+ irc_send_reply (c, IRC_ERR_NOADMININFO, c->ctx->server_name);
+}
+
+static void
+irc_handle_stats_links (struct client *c, const struct irc_message *msg)
+{
+ // There is only an "l" query in RFC 2812 but we cannot link,
+ // so instead we provide the "L" query giving information for all users
+ const char *filter = NULL;
+ if (msg->params.len > 1)
+ filter = msg->params.vector[1];
+
+ for (struct client *iter = c->ctx->clients; iter; iter = iter->next)
+ {
+ if (filter && irc_strcmp (iter->nickname, filter))
+ continue;
+ irc_send_reply (c, IRC_RPL_STATSLINKINFO,
+ iter->address, // linkname
+ iter->write_buffer.len, // sendq
+ iter->n_sent_messages, iter->sent_bytes / 1024,
+ iter->n_received_messages, iter->received_bytes / 1024,
+ (long long) (time (NULL) - iter->opened));
+ }
+}
+
+static void
+irc_handle_stats_commands (struct client *c)
+{
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->handlers);
+ struct irc_command *handler;
+ while ((handler = str_map_iter_next (&iter)))
+ {
+ if (!handler->n_received)
+ continue;
+ irc_send_reply (c, IRC_RPL_STATSCOMMANDS, handler->name,
+ handler->n_received, handler->bytes_received, (size_t) 0);
+ }
+}
+
+static void
+irc_handle_stats_uptime (struct client *c)
+{
+ time_t uptime = time (NULL) - c->ctx->started;
+
+ int days = uptime / 60 / 60 / 24;
+ int hours = (uptime % (60 * 60 * 24)) / 60 / 60;
+ int mins = (uptime % (60 * 60)) / 60;
+ int secs = uptime % 60;
+
+ irc_send_reply (c, IRC_RPL_STATSUPTIME, days, hours, mins, secs);
+}
+
+static void
+irc_handle_stats (const struct irc_message *msg, struct client *c)
+{
+ char query = 0;
+ if (msg->params.len > 0)
+ query = *msg->params.vector[0];
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
+ if (!(c->mode & IRC_USER_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
+
+ switch (query)
+ {
+ case 'L': irc_handle_stats_links (c, msg); break;
+ case 'm': irc_handle_stats_commands (c); break;
+ case 'u': irc_handle_stats_uptime (c); break;
+ }
+
+ irc_send_reply (c, IRC_RPL_ENDOFSTATS, query ? query : '*');
+}
+
+static void
+irc_handle_links (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
+
+ const char *mask = "*";
+ if (msg->params.len > 0)
+ mask = msg->params.vector[msg->params.len > 1];
+
+ if (!irc_fnmatch (mask, c->ctx->server_name))
+ irc_send_reply (c, IRC_RPL_LINKS, mask,
+ c->ctx->server_name, 0 /* hop count */,
+ str_map_find (&c->ctx->config, "server_info"));
+ irc_send_reply (c, IRC_RPL_ENDOFLINKS, mask);
+}
+
+static void
+irc_handle_wallops (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 1)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+ if (!(c->mode & IRC_USER_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
+
+ const char *message = msg->params.vector[0];
+
+ // Our interpretation: anonymize the sender,
+ // and target all users who want to receive these messages
+ struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
+ struct client *target;
+ while ((target = str_map_iter_next (&iter)))
+ {
+ if (target != c && !(target->mode & IRC_USER_MODE_RX_WALLOPS))
+ continue;
+
+ client_send (target, ":%s WALLOPS :%s", c->ctx->server_name, message);
+ }
+}
+
+static void
+irc_handle_kill (const struct irc_message *msg, struct client *c)
+{
+ if (msg->params.len < 2)
+ RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
+ if (!(c->mode & IRC_USER_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
+
+ struct client *target;
+ if (!(target = str_map_find (&c->ctx->users, msg->params.vector[0])))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, msg->params.vector[0]);
+
+ client_send (target, ":%s!%s@%s KILL %s :%s",
+ c->nickname, c->username, c->hostname,
+ target->nickname, msg->params.vector[1]);
+
+ char *reason = xstrdup_printf ("Killed by %s: %s",
+ c->nickname, msg->params.vector[1]);
+ client_close_link (target, reason);
+ free (reason);
+}
+
+static void
+irc_handle_die (const struct irc_message *msg, struct client *c)
+{
+ (void) msg;
+
+ if (!(c->mode & IRC_USER_MODE_OPERATOR))
+ RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
+ if (!c->ctx->quitting)
+ irc_initiate_quit (c->ctx);
+}
+
+// -----------------------------------------------------------------------------
+
+static void
+irc_register_handlers (struct server_context *ctx)
+{
+ // TODO: add an index for IRC_ERR_NOSUCHSERVER validation?
+ // TODO: add a minimal parameter count?
+ // TODO: add a field for oper-only commands?
+ static struct irc_command message_handlers[] =
+ {
+ { "CAP", false, irc_handle_cap, 0, 0 },
+ { "PASS", false, irc_handle_pass, 0, 0 },
+ { "NICK", false, irc_handle_nick, 0, 0 },
+ { "USER", false, irc_handle_user, 0, 0 },
+
+ { "USERHOST", true, irc_handle_userhost, 0, 0 },
+ { "LUSERS", true, irc_handle_lusers, 0, 0 },
+ { "MOTD", true, irc_handle_motd, 0, 0 },
+ { "PING", true, irc_handle_ping, 0, 0 },
+ { "PONG", false, irc_handle_pong, 0, 0 },
+ { "QUIT", false, irc_handle_quit, 0, 0 },
+ { "TIME", true, irc_handle_time, 0, 0 },
+ { "VERSION", true, irc_handle_version, 0, 0 },
+ { "USERS", true, irc_handle_users, 0, 0 },
+ { "SUMMON", true, irc_handle_summon, 0, 0 },
+ { "AWAY", true, irc_handle_away, 0, 0 },
+ { "ADMIN", true, irc_handle_admin, 0, 0 },
+ { "STATS", true, irc_handle_stats, 0, 0 },
+ { "LINKS", true, irc_handle_links, 0, 0 },
+ { "WALLOPS", true, irc_handle_wallops, 0, 0 },
+
+ { "MODE", true, irc_handle_mode, 0, 0 },
+ { "PRIVMSG", true, irc_handle_privmsg, 0, 0 },
+ { "NOTICE", true, irc_handle_notice, 0, 0 },
+ { "JOIN", true, irc_handle_join, 0, 0 },
+ { "PART", true, irc_handle_part, 0, 0 },
+ { "KICK", true, irc_handle_kick, 0, 0 },
+ { "INVITE", true, irc_handle_invite, 0, 0 },
+ { "TOPIC", true, irc_handle_topic, 0, 0 },
+ { "LIST", true, irc_handle_list, 0, 0 },
+ { "NAMES", true, irc_handle_names, 0, 0 },
+ { "WHO", true, irc_handle_who, 0, 0 },
+ { "WHOIS", true, irc_handle_whois, 0, 0 },
+ { "WHOWAS", true, irc_handle_whowas, 0, 0 },
+ { "ISON", true, irc_handle_ison, 0, 0 },
+
+ { "KILL", true, irc_handle_kill, 0, 0 },
+ { "DIE", true, irc_handle_die, 0, 0 },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS (message_handlers); i++)
+ {
+ const struct irc_command *cmd = &message_handlers[i];
+ str_map_set (&ctx->handlers, cmd->name, (void *) cmd);
+ }
+}
+
+static void
+irc_process_message (const struct irc_message *msg,
+ const char *raw, void *user_data)
+{
+ struct client *c = user_data;
+ if (c->closing_link)
+ return;
+
+ c->n_received_messages++;
+ c->received_bytes += strlen (raw) + 2;
+
+ if (!flood_detector_check (&c->antiflood))
+ {
+ client_close_link (c, "Excess flood");
+ return;
+ }
+
+ struct irc_command *cmd = str_map_find (&c->ctx->handlers, msg->command);
+ if (!cmd)
+ irc_send_reply (c, IRC_ERR_UNKNOWNCOMMAND, msg->command);
+ else
+ {
+ cmd->n_received++;
+ cmd->bytes_received += strlen (raw) + 2;
+
+ if (cmd->requires_registration && !c->registered)
+ irc_send_reply (c, IRC_ERR_NOTREGISTERED);
+ else
+ cmd->handler (msg, c);
+ }
+}
+
+// --- Network I/O -------------------------------------------------------------
+
+static bool
+irc_try_read (struct client *c)
+{
+ struct str *buf = &c->read_buffer;
+ ssize_t n_read;
+
+ while (true)
+ {
+ str_reserve (buf, 512);
+ n_read = read (c->socket_fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */);
+
+ if (n_read > 0)
+ {
+ buf->str[buf->len += n_read] = '\0';
+ // TODO: discard characters above the 512 character limit
+ // FIXME: we should probably discard the data if closing_link
+ irc_process_buffer (buf, irc_process_message, c);
+ continue;
+ }
+ if (n_read == 0)
+ {
+ client_kill (c, NULL);
+ return false;
+ }
+
+ if (errno == EAGAIN)
+ return true;
+ if (errno == EINTR)
+ continue;
+
+ print_debug ("%s: %s: %s", __func__, "read", strerror (errno));
+ client_kill (c, strerror (errno));
+ return false;
+ }
+}
+
+static bool
+irc_try_read_tls (struct client *c)
+{
+ if (c->ssl_tx_want_rx)
+ return true;
+
+ struct str *buf = &c->read_buffer;
+ c->ssl_rx_want_tx = false;
+ while (true)
+ {
+ str_reserve (buf, 512);
+ ERR_clear_error ();
+ int n_read = SSL_read (c->ssl, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (c->ssl, n_read, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ buf->str[buf->len += n_read] = '\0';
+ // TODO: discard characters above the 512 character limit
+ // FIXME: we should probably discard the data if closing_link
+ irc_process_buffer (buf, irc_process_message, c);
+ continue;
+ case SSL_ERROR_ZERO_RETURN:
+ client_kill (c, NULL);
+ return false;
+ case SSL_ERROR_WANT_READ:
+ return true;
+ case SSL_ERROR_WANT_WRITE:
+ c->ssl_rx_want_tx = true;
+ return true;
+ case XSSL_ERROR_TRY_AGAIN:
+ continue;
+ default:
+ print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
+ client_kill (c, error_info);
+ return false;
+ }
+ }
+}
+
+static bool
+irc_try_write (struct client *c)
+{
+ struct str *buf = &c->write_buffer;
+ ssize_t n_written;
+
+ while (buf->len)
+ {
+ n_written = write (c->socket_fd, buf->str, buf->len);
+ if (n_written >= 0)
+ {
+ str_remove_slice (buf, 0, n_written);
+ continue;
+ }
+
+ if (errno == EAGAIN)
+ return true;
+ if (errno == EINTR)
+ continue;
+
+ print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
+ client_kill (c, strerror (errno));
+ return false;
+ }
+ return true;
+}
+
+static bool
+irc_try_write_tls (struct client *c)
+{
+ if (c->ssl_rx_want_tx)
+ return true;
+
+ struct str *buf = &c->write_buffer;
+ c->ssl_tx_want_rx = false;
+ while (buf->len)
+ {
+ ERR_clear_error ();
+ int n_written = SSL_write (c->ssl, buf->str, buf->len);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (c->ssl, n_written, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ str_remove_slice (buf, 0, n_written);
+ continue;
+ case SSL_ERROR_ZERO_RETURN:
+ client_kill (c, NULL);
+ return false;
+ case SSL_ERROR_WANT_WRITE:
+ return true;
+ case SSL_ERROR_WANT_READ:
+ c->ssl_tx_want_rx = true;
+ return true;
+ case XSSL_ERROR_TRY_AGAIN:
+ continue;
+ default:
+ print_debug ("%s: %s: %s", __func__, "SSL_write", error_info);
+ client_kill (c, error_info);
+ return false;
+ }
+ }
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+
+static bool
+irc_autodetect_tls (struct client *c)
+{
+ // Trivial SSL/TLS autodetection. The first block of data returned by
+ // recv() must be at least three bytes long for this to work reliably,
+ // but that should not pose a problem in practice.
+ //
+ // SSL2: 1xxx xxxx | xxxx xxxx | <1>
+ // (message length) (client hello)
+ // SSL3/TLS: <22> | <3> | xxxx xxxx
+ // (handshake)| (protocol version)
+ //
+ // Such byte sequences should never occur at the beginning of regular IRC
+ // communication, which usually begins with USER/NICK/PASS/SERVICE.
+
+ char buf[3];
+start:
+ switch (recv (c->socket_fd, buf, sizeof buf, MSG_PEEK))
+ {
+ case 3:
+ if ((buf[0] & 0x80) && buf[2] == 1)
+ return true;
+ // Fall-through
+ case 2:
+ if (buf[0] == 22 && buf[1] == 3)
+ return true;
+ break;
+ case 1:
+ if (buf[0] == 22)
+ return true;
+ break;
+ case 0:
+ break;
+ default:
+ if (errno == EINTR)
+ goto start;
+ }
+ return false;
+}
+
+static bool
+client_initialize_tls (struct client *c)
+{
+ const char *error_info = NULL;
+ if (!c->ctx->ssl_ctx)
+ {
+ error_info = "TLS support disabled";
+ goto error_ssl_1;
+ }
+
+ ERR_clear_error ();
+
+ c->ssl = SSL_new (c->ctx->ssl_ctx);
+ if (!c->ssl)
+ goto error_ssl_2;
+ if (!SSL_set_fd (c->ssl, c->socket_fd))
+ goto error_ssl_3;
+
+ SSL_set_accept_state (c->ssl);
+ return true;
+
+error_ssl_3:
+ SSL_free (c->ssl);
+ c->ssl = NULL;
+error_ssl_2:
+ error_info = xerr_describe_error ();
+error_ssl_1:
+ print_debug ("could not initialize TLS for %s: %s", c->address, error_info);
+ return false;
+}
+
+// -----------------------------------------------------------------------------
+
+static void
+on_client_ready (const struct pollfd *pfd, void *user_data)
+{
+ struct client *c = user_data;
+ if (!c->initialized)
+ {
+ hard_assert (pfd->events == POLLIN);
+ if (irc_autodetect_tls (c) && !client_initialize_tls (c))
+ {
+ client_kill (c, NULL);
+ return;
+ }
+ c->initialized = true;
+ client_set_ping_timer (c);
+ }
+
+ if (c->ssl)
+ {
+ // Reads may want to write, writes may want to read, poll() may
+ // return unexpected things in `revents'... let's try both
+ if (!irc_try_read_tls (c) || !irc_try_write_tls (c))
+ return;
+ }
+ else if (!irc_try_read (c) || !irc_try_write (c))
+ return;
+
+ client_update_poller (c, pfd);
+
+ // The purpose of the `closing_link' state is to transfer the `ERROR'
+ if (c->closing_link && !c->half_closed && !c->write_buffer.len)
+ {
+ // To make sure the client has received our ERROR message, we must
+ // first half-close the connection, otherwise it could happen that they
+ // receive a RST from our TCP stack first when we receive further data
+
+ // We only send the "close notify" alert if libssl can write to the
+ // socket at this moment. All the other data has been already written,
+ // though, and the client will receive a TCP half-close as usual, so
+ // it's not that important if the alert actually gets through.
+ if (c->ssl)
+ (void) SSL_shutdown (c->ssl);
+
+ // Either the shutdown succeeds, in which case we set a flag so that
+ // we don't retry this action and wait until we get an EOF, or it fails
+ // and we just kill the client straight away
+ if (!shutdown (c->socket_fd, SHUT_WR))
+ c->half_closed = true;
+ else
+ client_kill (c, NULL);
+ }
+}
+
+static void
+client_update_poller (struct client *c, const struct pollfd *pfd)
+{
+ // We must not poll for writing when the connection hasn't been initialized
+ int new_events = POLLIN;
+ if (c->ssl)
+ {
+ if (c->write_buffer.len || c->ssl_rx_want_tx)
+ new_events |= POLLOUT;
+
+ // While we're waiting for an opposite event, we ignore the original
+ if (c->ssl_rx_want_tx) new_events &= ~POLLIN;
+ if (c->ssl_tx_want_rx) new_events &= ~POLLOUT;
+ }
+ else if (c->initialized && c->write_buffer.len)
+ new_events |= POLLOUT;
+
+ hard_assert (new_events != 0);
+ if (!pfd || pfd->events != new_events)
+ poller_fd_set (&c->socket_event, new_events);
+}
+
+static void
+client_finish_connection (struct client *c)
+{
+ c->gni = NULL;
+
+ c->address = format_host_port_pair (c->hostname, c->port);
+ print_debug ("accepted connection from %s", c->address);
+
+ client_update_poller (c, NULL);
+ client_set_kill_timer (c);
+}
+
+static void
+on_client_gni_resolved (int result, char *host, char *port, void *user_data)
+{
+ struct client *c = user_data;
+
+ if (result)
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (result));
+ else
+ {
+ cstr_set (&c->hostname, xstrdup (host));
+ (void) port;
+ }
+
+ poller_timer_reset (&c->gni_timer);
+ client_finish_connection (c);
+}
+
+static void
+on_client_gni_timer (struct client *c)
+{
+ async_cancel (&c->gni->async);
+ client_finish_connection (c);
+}
+
+static bool
+irc_try_fetch_client (struct server_context *ctx, int listen_fd)
+{
+ // XXX: `struct sockaddr_storage' is not the most portable thing
+ struct sockaddr_storage peer;
+ socklen_t peer_len = sizeof peer;
+
+ int fd = accept (listen_fd, (struct sockaddr *) &peer, &peer_len);
+ if (fd == -1)
+ {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return false;
+ if (errno == EINTR)
+ return true;
+
+ if (accept_error_is_transient (errno))
+ print_warning ("%s: %s", "accept", strerror (errno));
+ else
+ print_fatal ("%s: %s", "accept", strerror (errno));
+ return true;
+ }
+
+ hard_assert (peer_len <= sizeof peer);
+ set_blocking (fd, false);
+
+ // A little bit questionable once the traffic gets high enough (IMO),
+ // but it reduces silly latencies that we don't need because we already
+ // do buffer our output
+ int yes = 1;
+ soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
+ &yes, sizeof yes) != -1);
+
+ if (ctx->max_connections != 0 && ctx->n_clients >= ctx->max_connections)
+ {
+ print_debug ("connection limit reached, refusing connection");
+ close (fd);
+ return true;
+ }
+
+ char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown";
+ int err = getnameinfo ((struct sockaddr *) &peer, peer_len,
+ host, sizeof host, port, sizeof port, NI_NUMERICHOST | NI_NUMERICSERV);
+ if (err)
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
+
+ struct client *c = client_new ();
+ c->ctx = ctx;
+ c->opened = time (NULL);
+ c->socket_fd = fd;
+ c->hostname = xstrdup (host);
+ c->port = xstrdup (port);
+ c->last_active = time (NULL);
+ LIST_PREPEND (ctx->clients, c);
+ ctx->n_clients++;
+
+ c->socket_event = poller_fd_make (&c->ctx->poller, c->socket_fd);
+ c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
+ c->socket_event.user_data = c;
+
+ c->kill_timer = poller_timer_make (&c->ctx->poller);
+ c->kill_timer.dispatcher = (poller_timer_fn) on_client_kill_timer;
+ c->kill_timer.user_data = c;
+
+ c->timeout_timer = poller_timer_make (&c->ctx->poller);
+ c->timeout_timer.dispatcher = (poller_timer_fn) on_client_timeout_timer;
+ c->timeout_timer.user_data = c;
+
+ c->ping_timer = poller_timer_make (&c->ctx->poller);
+ c->ping_timer.dispatcher = (poller_timer_fn) on_client_ping_timer;
+ c->ping_timer.user_data = c;
+
+ // Resolve the client's hostname first; this is a blocking operation that
+ // depends on the network, so run it asynchronously with some timeout
+ c->gni = async_getnameinfo (&ctx->poller.common.async,
+ (const struct sockaddr *) &peer, peer_len, NI_NUMERICSERV);
+ c->gni->dispatcher = on_client_gni_resolved;
+ c->gni->user_data = c;
+
+ c->gni_timer = poller_timer_make (&c->ctx->poller);
+ c->gni_timer.dispatcher = (poller_timer_fn) on_client_gni_timer;
+ c->gni_timer.user_data = c;
+
+ poller_timer_set (&c->gni_timer, 5000);
+ return true;
+}
+
+static void
+on_irc_client_available (const struct pollfd *pfd, void *user_data)
+{
+ struct server_context *ctx = user_data;
+ while (irc_try_fetch_client (ctx, pfd->fd))
+ ;
+}
+
+// --- Application setup -------------------------------------------------------
+
+static int
+irc_ssl_verify_callback (int verify_ok, X509_STORE_CTX *ctx)
+{
+ (void) verify_ok;
+ (void) ctx;
+
+ // RFC 5246: "If the client has sent a certificate with signing ability,
+ // a digitally-signed CertificateVerify message is sent to explicitly
+ // verify possession of the private key in the certificate."
+ //
+ // The handshake will fail if the client doesn't have a matching private
+ // key, see OpenSSL's tls_process_cert_verify(), and the CertificateVerify
+ // message cannot be skipped (except for a case where it doesn't matter).
+ // Thus we're fine checking just the cryptographic hash of the certificate.
+
+ // We only want to provide additional privileges based on the client's
+ // certificate, so let's not terminate the connection because of a failure
+ // (especially since self-signed certificates are likely to be used).
+ return 1;
+}
+
+static void
+irc_ssl_info_callback (const SSL *ssl, int where, int ret)
+{
+ // For debugging only; provides us with the most important information
+
+ struct str s = str_make ();
+ if (where & SSL_CB_LOOP)
+ str_append_printf (&s, "loop (%s) ",
+ SSL_state_string_long (ssl));
+ if (where & SSL_CB_EXIT)
+ str_append_printf (&s, "exit (%d in %s) ", ret,
+ SSL_state_string_long (ssl));
+
+ if (where & SSL_CB_READ) str_append (&s, "read ");
+ if (where & SSL_CB_WRITE) str_append (&s, "write ");
+
+ if (where & SSL_CB_ALERT)
+ str_append_printf (&s, "alert (%s: %s) ",
+ SSL_alert_type_string_long (ret),
+ SSL_alert_desc_string_long (ret));
+
+ if (where & SSL_CB_HANDSHAKE_START) str_append (&s, "handshake start ");
+ if (where & SSL_CB_HANDSHAKE_DONE) str_append (&s, "handshake done ");
+
+ print_debug ("ssl <%p> %s", ssl, s.str);
+ str_free (&s);
+}
+
+static bool
+irc_initialize_ssl_ctx (struct server_context *ctx,
+ const char *cert_path, const char *key_path, struct error **e)
+{
+ ERR_clear_error ();
+
+ ctx->ssl_ctx = SSL_CTX_new (SSLv23_server_method ());
+ if (!ctx->ssl_ctx)
+ {
+ error_set (e, "%s: %s", "could not initialize TLS",
+ xerr_describe_error ());
+ return false;
+ }
+ SSL_CTX_set_verify (ctx->ssl_ctx,
+ SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, irc_ssl_verify_callback);
+
+ if (g_debug_mode)
+ SSL_CTX_set_info_callback (ctx->ssl_ctx, irc_ssl_info_callback);
+
+ const unsigned char session_id_context[SSL_MAX_SSL_SESSION_ID_LENGTH]
+ = PROGRAM_NAME;
+ (void) SSL_CTX_set_session_id_context (ctx->ssl_ctx,
+ session_id_context, sizeof session_id_context);
+
+ // IRC is not particularly reconnect-heavy, prefer forward secrecy
+ SSL_CTX_set_session_cache_mode (ctx->ssl_ctx, SSL_SESS_CACHE_OFF);
+
+ // Gah, spare me your awkward semantics, I just want to push data!
+ SSL_CTX_set_mode (ctx->ssl_ctx,
+ SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+ // Disable deprecated protocols (see RFC 7568)
+ SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ // XXX: perhaps we should read the files ourselves for better messages
+ const char *ciphers = str_map_find (&ctx->config, "tls_ciphers");
+ if (!SSL_CTX_set_cipher_list (ctx->ssl_ctx, ciphers))
+ error_set (e, "failed to select any cipher from the cipher list");
+ else if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path))
+ error_set (e, "%s: %s", "setting the TLS certificate failed",
+ xerr_describe_error ());
+ else if (!SSL_CTX_use_PrivateKey_file
+ (ctx->ssl_ctx, key_path, SSL_FILETYPE_PEM))
+ error_set (e, "%s: %s", "setting the TLS private key failed",
+ xerr_describe_error ());
+ else
+ // TODO: SSL_CTX_check_private_key()? It has probably already been
+ // checked by SSL_CTX_use_PrivateKey_file() above.
+ return true;
+
+ SSL_CTX_free (ctx->ssl_ctx);
+ ctx->ssl_ctx = NULL;
+ return false;
+}
+
+static bool
+irc_initialize_tls (struct server_context *ctx, struct error **e)
+{
+ const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
+ const char *tls_key = str_map_find (&ctx->config, "tls_key");
+
+ // Only try to enable SSL support if the user configures it; it is not
+ // a failure if no one has requested it.
+ if (!tls_cert && !tls_key)
+ return true;
+
+ if (!tls_cert)
+ error_set (e, "no TLS certificate set");
+ else if (!tls_key)
+ error_set (e, "no TLS private key set");
+ if (!tls_cert || !tls_key)
+ return false;
+
+ bool result = false;
+
+ char *cert_path = resolve_filename
+ (tls_cert, resolve_relative_config_filename);
+ char *key_path = resolve_filename
+ (tls_key, resolve_relative_config_filename);
+ if (!cert_path)
+ error_set (e, "%s: %s", "cannot open file", tls_cert);
+ else if (!key_path)
+ error_set (e, "%s: %s", "cannot open file", tls_key);
+ else
+ result = irc_initialize_ssl_ctx (ctx, cert_path, key_path, e);
+
+ free (cert_path);
+ free (key_path);
+ return result;
+}
+
+static bool
+irc_initialize_catalog (struct server_context *ctx, struct error **e)
+{
+ hard_assert (ctx->catalog == (nl_catd) -1);
+ const char *catalog = str_map_find (&ctx->config, "catalog");
+ if (!catalog)
+ return true;
+
+ char *path = resolve_filename (catalog, resolve_relative_config_filename);
+ if (!path)
+ {
+ error_set (e, "%s: %s", "cannot open file", catalog);
+ return false;
+ }
+ ctx->catalog = catopen (path, NL_CAT_LOCALE);
+ free (path);
+
+ if (ctx->catalog == (nl_catd) -1)
+ {
+ error_set (e, "%s: %s",
+ "failed reading the message catalog file", strerror (errno));
+ return false;
+ }
+ return true;
+}
+
+static bool
+irc_initialize_motd (struct server_context *ctx, struct error **e)
+{
+ hard_assert (ctx->motd.len == 0);
+ const char *motd = str_map_find (&ctx->config, "motd");
+ if (!motd)
+ return true;
+
+ char *path = resolve_filename (motd, resolve_relative_config_filename);
+ if (!path)
+ {
+ error_set (e, "%s: %s", "cannot open file", motd);
+ return false;
+ }
+ FILE *fp = fopen (path, "r");
+ free (path);
+
+ if (!fp)
+ {
+ error_set (e, "%s: %s",
+ "failed reading the MOTD file", strerror (errno));
+ return false;
+ }
+
+ struct str line = str_make ();
+ while (read_line (fp, &line))
+ strv_append_owned (&ctx->motd, str_steal (&line));
+ str_free (&line);
+
+ fclose (fp);
+ return true;
+}
+
+static bool
+irc_parse_config_unsigned (const char *name, const char *value, unsigned *out,
+ unsigned long min, unsigned long max, struct error **e)
+{
+ unsigned long ul;
+ hard_assert (value != NULL);
+ if (!xstrtoul (&ul, value, 10) || ul > max || ul < min)
+ {
+ error_set (e, "invalid configuration value for `%s': %s",
+ name, "the number is invalid or out of range");
+ return false;
+ }
+ *out = ul;
+ return true;
+}
+
+/// This function handles values that require validation before their first use,
+/// or some kind of a transformation (such as conversion to an integer) needs
+/// to be done before they can be used directly.
+static bool
+irc_parse_config (struct server_context *ctx, struct error **e)
+{
+#define PARSE_UNSIGNED(name, min, max) \
+ irc_parse_config_unsigned (#name, str_map_find (&ctx->config, #name), \
+ &ctx->name, min, max, e)
+
+ if (!PARSE_UNSIGNED (ping_interval, 1, UINT_MAX)
+ || !PARSE_UNSIGNED (max_connections, 0, UINT_MAX))
+ return false;
+
+ bool result = true;
+ struct strv fingerprints = strv_make ();
+ const char *operators = str_map_find (&ctx->config, "operators");
+ if (operators)
+ cstr_split (operators, ",", true, &fingerprints);
+ for (size_t i = 0; i < fingerprints.len; i++)
+ {
+ const char *key = fingerprints.vector[i];
+ if (!irc_is_valid_fingerprint (key))
+ {
+ error_set (e, "invalid configuration value for `%s': %s",
+ "operators", "invalid fingerprint value");
+ result = false;
+ break;
+ }
+ str_map_set (&ctx->operators, key, (void *) 1);
+ }
+ strv_free (&fingerprints);
+ return result;
+}
+
+static bool
+irc_initialize_server_name (struct server_context *ctx, struct error **e)
+{
+ enum validation_result res;
+ const char *server_name = str_map_find (&ctx->config, "server_name");
+ if (server_name)
+ {
+ res = irc_validate_hostname (server_name);
+ if (res != VALIDATION_OK)
+ {
+ error_set (e, "invalid configuration value for `%s': %s",
+ "server_name", irc_validate_to_str (res));
+ return false;
+ }
+ ctx->server_name = xstrdup (server_name);
+ }
+ else
+ {
+ long host_name_max = sysconf (_SC_HOST_NAME_MAX);
+ if (host_name_max <= 0)
+ host_name_max = _POSIX_HOST_NAME_MAX;
+
+ char hostname[host_name_max + 1];
+ if (gethostname (hostname, sizeof hostname))
+ {
+ error_set (e, "%s: %s",
+ "getting the hostname failed", strerror (errno));
+ return false;
+ }
+ res = irc_validate_hostname (hostname);
+ if (res != VALIDATION_OK)
+ {
+ error_set (e,
+ "`%s' is not set and the hostname (`%s') cannot be used: %s",
+ "server_name", hostname, irc_validate_to_str (res));
+ return false;
+ }
+ ctx->server_name = xstrdup (hostname);
+ }
+ return true;
+}
+
+static bool
+irc_lock_pid_file (struct server_context *ctx, struct error **e)
+{
+ const char *path = str_map_find (&ctx->config, "pid_file");
+ if (!path)
+ return true;
+
+ char *resolved = resolve_filename (path, resolve_relative_runtime_filename);
+ bool result = lock_pid_file (resolved, e) != -1;
+ free (resolved);
+ return result;
+}
+
+static int
+irc_listen (struct addrinfo *ai)
+{
+ int fd = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (fd == -1)
+ return -1;
+ set_cloexec (fd);
+
+ int yes = 1;
+ soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
+ &yes, sizeof yes) != -1);
+ soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR,
+ &yes, sizeof yes) != -1);
+
+#if defined SOL_IPV6 && defined IPV6_V6ONLY
+ // Make NULL always bind to both IPv4 and IPv6, irrespectively of the order
+ // of results; only INADDR6_ANY seems to be affected by this
+ if (ai->ai_family == AF_INET6)
+ soft_assert (setsockopt (fd, SOL_IPV6, IPV6_V6ONLY,
+ &yes, sizeof yes) != -1);
+#endif
+
+ char *address = gai_reconstruct_address (ai);
+ if (bind (fd, ai->ai_addr, ai->ai_addrlen))
+ print_error ("bind to %s failed: %s", address, strerror (errno));
+ else if (listen (fd, 16 /* arbitrary number */))
+ print_error ("listen on %s failed: %s", address, strerror (errno));
+ else
+ {
+ print_status ("listening on %s", address);
+ free (address);
+ return fd;
+ }
+
+ free (address);
+ xclose (fd);
+ return -1;
+}
+
+static void
+irc_listen_resolve (struct server_context *ctx,
+ const char *host, const char *port, struct addrinfo *gai_hints)
+{
+ struct addrinfo *gai_result = NULL, *gai_iter = NULL;
+ int err = getaddrinfo (host, port, gai_hints, &gai_result);
+ if (err)
+ {
+ char *address = format_host_port_pair (host, port);
+ print_error ("binding to %s failed: %s: %s",
+ address, "getaddrinfo", gai_strerror (err));
+ free (address);
+ return;
+ }
+
+ int fd;
+ for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
+ {
+ if (ctx->listen_len == ctx->listen_alloc)
+ break;
+
+ if ((fd = irc_listen (gai_iter)) == -1)
+ continue;
+ set_blocking (fd, false);
+
+ struct poller_fd *event = &ctx->listen_events[ctx->listen_len];
+ *event = poller_fd_make (&ctx->poller, fd);
+ event->dispatcher = (poller_fd_fn) on_irc_client_available;
+ event->user_data = ctx;
+
+ ctx->listen_fds[ctx->listen_len++] = fd;
+ poller_fd_set (event, POLLIN);
+ }
+ freeaddrinfo (gai_result);
+}
+
+static bool
+irc_setup_listen_fds (struct server_context *ctx, struct error **e)
+{
+ const char *bind_host = str_map_find (&ctx->config, "bind_host");
+ const char *bind_port = str_map_find (&ctx->config, "bind_port");
+ hard_assert (bind_port != NULL); // We have a default value for this
+
+ struct addrinfo gai_hints;
+ memset (&gai_hints, 0, sizeof gai_hints);
+
+ gai_hints.ai_socktype = SOCK_STREAM;
+ gai_hints.ai_flags = AI_PASSIVE;
+
+ struct strv ports = strv_make ();
+ cstr_split (bind_port, ",", true, &ports);
+
+ // For C and simplicity's sake let's assume that the host will resolve
+ // to at most two different addresses: IPv4 and IPv6 in case it is NULL
+ ctx->listen_alloc = ports.len * 2;
+
+ ctx->listen_fds =
+ xcalloc (ctx->listen_alloc, sizeof *ctx->listen_fds);
+ ctx->listen_events =
+ xcalloc (ctx->listen_alloc, sizeof *ctx->listen_events);
+ for (size_t i = 0; i < ports.len; i++)
+ irc_listen_resolve (ctx, bind_host, ports.vector[i], &gai_hints);
+ strv_free (&ports);
+
+ if (!ctx->listen_len)
+ {
+ error_set (e, "%s: %s",
+ "network setup failed", "no ports to listen on");
+ return false;
+ }
+ return true;
+}
+
+// --- Main --------------------------------------------------------------------
+
+static void
+on_signal_pipe_readable (const struct pollfd *fd, struct server_context *ctx)
+{
+ char dummy;
+ (void) read (fd->fd, &dummy, 1);
+
+ if (g_termination_requested && !ctx->quitting)
+ irc_initiate_quit (ctx);
+}
+
+static void
+daemonize (struct server_context *ctx)
+{
+ print_status ("daemonizing...");
+
+ if (chdir ("/"))
+ exit_fatal ("%s: %s", "chdir", strerror (errno));
+
+ // Because of systemd, we need to exit the parent process _after_ writing
+ // a PID file, otherwise our grandchild would receive a SIGTERM
+ int sync_pipe[2];
+ if (pipe (sync_pipe))
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ pid_t pid;
+ if ((pid = fork ()) < 0)
+ exit_fatal ("%s: %s", "fork", strerror (errno));
+ else if (pid)
+ {
+ // Wait until all write ends of the pipe are closed, which can mean
+ // either success or failure, we don't need to care
+ xclose (sync_pipe[PIPE_WRITE]);
+
+ char dummy;
+ if (read (sync_pipe[PIPE_READ], &dummy, 1) < 0)
+ exit_fatal ("%s: %s", "read", strerror (errno));
+
+ exit (EXIT_SUCCESS);
+ }
+
+ setsid ();
+ signal (SIGHUP, SIG_IGN);
+
+ if ((pid = fork ()) < 0)
+ exit_fatal ("%s: %s", "fork", strerror (errno));
+ else if (pid)
+ exit (EXIT_SUCCESS);
+
+ openlog (PROGRAM_NAME, LOG_NDELAY | LOG_NOWAIT | LOG_PID, 0);
+ g_log_message_real = log_message_syslog;
+
+ // Write the PID file (if so configured) and get rid of the pipe, so that
+ // the read() in our grandparent finally returns zero (no write ends)
+ struct error *e = NULL;
+ if (!irc_lock_pid_file (ctx, &e))
+ exit_fatal ("%s", e->message);
+
+ xclose (sync_pipe[PIPE_READ]);
+ xclose (sync_pipe[PIPE_WRITE]);
+
+ // XXX: we may close our own descriptors this way, crippling ourselves;
+ // there is no real guarantee that we will start with all three
+ // descriptors open. In theory we could try to enumerate the descriptors
+ // at the start of main().
+ for (int i = 0; i < 3; i++)
+ xclose (i);
+
+ int tty = open ("/dev/null", O_RDWR);
+ if (tty != 0 || dup (0) != 1 || dup (0) != 2)
+ exit_fatal ("failed to reopen FD's: %s", strerror (errno));
+
+ poller_post_fork (&ctx->poller);
+}
+
+static void
+setup_limits (void)
+{
+ struct rlimit limit;
+ if (getrlimit (RLIMIT_NOFILE, &limit))
+ {
+ print_warning ("%s: %s", "getrlimit", strerror (errno));
+ return;
+ }
+
+ limit.rlim_cur = limit.rlim_max;
+ if (setrlimit (RLIMIT_NOFILE, &limit))
+ print_warning ("%s: %s", "setrlimit", strerror (errno));
+}
+
+int
+main (int argc, char *argv[])
+{
+ // Need to call this first as all string maps depend on it
+ siphash_wrapper_randomize ();
+
+ static const struct opt opts[] =
+ {
+ { 'd', "debug", NULL, 0, "run in debug mode (do not daemonize)" },
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ { 'w', "write-default-cfg", "FILENAME",
+ OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
+ "write a default configuration file and exit" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh =
+ opt_handler_make (argc, argv, opts, NULL, "IRC daemon.");
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'd':
+ g_debug_mode = true;
+ break;
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ case 'w':
+ call_simple_config_write_default (optarg, g_config_table);
+ exit (EXIT_SUCCESS);
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ opt_handler_free (&oh);
+
+ print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
+ setup_signal_handlers ();
+ setup_limits ();
+ init_openssl ();
+
+ struct server_context ctx;
+ server_context_init (&ctx);
+ ctx.started = time (NULL);
+ irc_register_handlers (&ctx);
+ irc_register_cap_handlers (&ctx);
+
+ struct error *e = NULL;
+ if (!simple_config_update_from_file (&ctx.config, &e))
+ {
+ print_error ("error loading configuration: %s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ ctx.signal_event = poller_fd_make (&ctx.poller, g_signal_pipe[0]);
+ ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
+ ctx.signal_event.user_data = &ctx;
+ poller_fd_set (&ctx.signal_event, POLLIN);
+
+ if (!irc_initialize_tls (&ctx, &e)
+ || !irc_initialize_server_name (&ctx, &e)
+ || !irc_initialize_motd (&ctx, &e)
+ || !irc_initialize_catalog (&ctx, &e)
+ || !irc_parse_config (&ctx, &e)
+ || !irc_setup_listen_fds (&ctx, &e))
+ exit_fatal ("%s", e->message);
+
+ if (!g_debug_mode)
+ daemonize (&ctx);
+ else if (!irc_lock_pid_file (&ctx, &e))
+ exit_fatal ("%s", e->message);
+
+#if OpenBSD >= 201605
+ // This won't be as simple once we decide to implement REHASH
+ if (pledge ("stdio inet dns", NULL))
+ exit_fatal ("%s: %s", "pledge", strerror (errno));
+#endif
+
+ ctx.polling = true;
+ while (ctx.polling)
+ poller_run (&ctx.poller);
+
+ server_context_free (&ctx);
+ return EXIT_SUCCESS;
+}
diff --git a/xF.c b/xF.c
new file mode 100644
index 0000000..054871d
--- /dev/null
+++ b/xF.c
@@ -0,0 +1,172 @@
+/*
+ * xF.c: a toothless IRC client frontend
+ *
+ * Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ */
+
+#include "config.h"
+#define PROGRAM_NAME "xF"
+
+#include "common.c"
+#include "xC-proto.c"
+
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/Xft/Xft.h>
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct
+{
+ bool polling;
+ struct connector connector;
+ int socket;
+}
+g;
+
+static void
+on_connector_connecting (void *user_data, const char *address)
+{
+ (void) user_data;
+ print_status ("connecting to %s...", address);
+}
+
+static void
+on_connector_error (void *user_data, const char *error)
+{
+ (void) user_data;
+ print_status ("connection failed: %s", error);
+}
+
+static void
+on_connector_failure (void *user_data)
+{
+ (void) user_data;
+ exit_fatal ("giving up");
+}
+
+static void
+on_connector_connected (void *user_data, int socket, const char *hostname)
+{
+ (void) user_data;
+ (void) hostname;
+ g.polling = false;
+ g.socket = socket;
+}
+
+static void
+protocol_test (const char *host, const char *port)
+{
+ struct poller poller = {};
+ poller_init (&poller);
+
+ connector_init (&g.connector, &poller);
+ g.connector.on_connecting = on_connector_connecting;
+ g.connector.on_error = on_connector_error;
+ g.connector.on_connected = on_connector_connected;
+ g.connector.on_failure = on_connector_failure;
+
+ connector_add_target (&g.connector, host, port);
+
+ g.polling = true;
+ while (g.polling)
+ poller_run (&poller);
+
+ connector_free (&g.connector);
+
+ struct str s = str_make ();
+ str_pack_u32 (&s, 0);
+ struct relay_command_message m = {};
+ m.data.hello.command = RELAY_COMMAND_HELLO;
+ m.data.hello.version = RELAY_VERSION;
+ if (!relay_command_message_serialize (&m, &s))
+ exit_fatal ("serialization failed");
+
+ uint32_t len = htonl (s.len - sizeof len);
+ memcpy (s.str, &len, sizeof len);
+ if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len)
+ exit_fatal ("short send or error: %s", strerror (errno));
+
+ char buf[1 << 20] = "";
+ while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len)
+ {
+ len = ntohl (len);
+ if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len)
+ exit_fatal ("short read or error: %s", strerror (errno));
+
+ struct msg_unpacker r = msg_unpacker_make (buf, len);
+ struct relay_event_message m = {};
+ if (!relay_event_message_deserialize (&m, &r))
+ exit_fatal ("deserialization failed");
+ if (msg_unpacker_get_available (&r))
+ exit_fatal ("trailing data");
+
+ printf ("event: %d\n", m.data.event);
+ relay_event_message_free (&m);
+ }
+ exit_fatal ("short read or error: %s", strerror (errno));
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+int
+main (int argc, char *argv[])
+{
+ static const struct opt opts[] =
+ {
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh = opt_handler_make (argc, argv, opts,
+ "HOST:PORT", "X11 frontend for xC.");
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ argc -= optind;
+ argv += optind;
+ if (argc != 1)
+ {
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+ opt_handler_free (&oh);
+
+ char *address = xstrdup (argv[0]);
+ const char *port = NULL, *host = tokenize_host_port (address, &port);
+ if (!port)
+ exit_fatal ("missing port number/service name");
+
+ // TODO: Actually implement an X11-based user interface.
+ protocol_test (host, port);
+ return 0;
+}
diff --git a/xF.svg b/xF.svg
new file mode 100644
index 0000000..75b7156
--- /dev/null
+++ b/xF.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+
+ <defs>
+ <linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
+ <stop stop-color="#808080" offset="0" />
+ <stop stop-color="#000000" offset="1" />
+ </linearGradient>
+ <!-- librsvg screws up the filter's orientation in a weird way
+ otherwise a larger blur value would look better -->
+ <filter id="shadow" color-interpolation-filters="sRGB">
+ <feOffset dy="0.5" />
+ <feGaussianBlur stdDeviation="0.5" />
+ <feComposite in2="SourceGraphic" operator="in" />
+ </filter>
+ <clipPath id="clip">
+ <rect x="-7" y="-10" width="14" height="20" />
+ </clipPath>
+ </defs>
+
+ <circle cx="24" cy="24" r="20"
+ fill="url(#background)" stroke="#404040" stroke-width="2" />
+
+ <g transform="rotate(-45 24 24)" filter="url(#shadow)">
+ <path d="m 12,25 h 24 v 11 h -5 v -8 h -4.5 v 6 h -5 v -6 h -9.5 z"
+ fill="#ffffff" />
+ <g stroke-width="4" transform="translate(24, 16)" clip-path="url(#clip)"
+ stroke="#ffffff">
+ <line x1="-8" x2="8" y1="-5" y2="5" />
+ <line x1="-8" x2="8" y1="5" y2="-5" />
+ </g>
+ </g>
+</svg>
diff --git a/xP.webp b/xP.webp
new file mode 100644
index 0000000..3c6028a
--- /dev/null
+++ b/xP.webp
Binary files differ
diff --git a/xP/.gitignore b/xP/.gitignore
new file mode 100644
index 0000000..ba4d8c3
--- /dev/null
+++ b/xP/.gitignore
@@ -0,0 +1,4 @@
+/xP
+/proto.go
+/public/proto.js
+/public/mithril.js
diff --git a/xP/Makefile b/xP/Makefile
new file mode 100644
index 0000000..34de55a
--- /dev/null
+++ b/xP/Makefile
@@ -0,0 +1,18 @@
+.POSIX:
+.SUFFIXES:
+
+outputs = xP proto.go public/proto.js public/mithril.js
+all: $(outputs) public/ircfmt.woff2
+
+xP: xP.go proto.go
+ go build -o $@
+proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
+ awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
+public/proto.js: ../xC-gen-proto.awk ../xC-gen-proto-js.awk ../xC-proto
+ awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-js.awk ../xC-proto > $@
+public/ircfmt.woff2: gen-ircfmt.awk
+ awk -v Output=$@ -f gen-ircfmt.awk
+public/mithril.js:
+ curl -Lo $@ https://unpkg.com/mithril/mithril.js
+clean:
+ rm -f $(outputs)
diff --git a/xP/gen-ircfmt.awk b/xP/gen-ircfmt.awk
new file mode 100644
index 0000000..cc9a5a0
--- /dev/null
+++ b/xP/gen-ircfmt.awk
@@ -0,0 +1,89 @@
+# gen-ircfmt.awk: generate a supplementary font for IRC formatting characters
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# Usage: awk -v Output=static/ircfmt.woff2 -f gen-ircfmt.awk
+# Clean up SVG byproducts yourself.
+
+BEGIN {
+ if (!Output) {
+ print "Error: you must specify the output filename"
+ exit 1
+ }
+}
+
+function glyph(name, code, path, filename, actions, cmd) {
+ filename = Output "." name ".svg"
+
+ # Inkscape still does a terrible job at the stroke-to-path conversion.
+ actions = \
+ "select-by-id:group;" \
+ "selection-ungroup;" \
+ "select-clear;" \
+ "select-by-id:path;" \
+ "object-stroke-to-path;" \
+ "select-by-id:clip;" \
+ "path-intersection;" \
+ "select-all;" \
+ "path-combine;" \
+ "export-overwrite;" \
+ "export-filename:" filename ";" \
+ "export-do"
+
+ # These dimensions fit FontForge defaults, and happen to work well.
+ cmd = "inkscape --pipe --actions='" actions "'"
+ print "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n" \
+ "<svg version='1.1' xmlns='http://www.w3.org/2000/svg'\n" \
+ " width='1000' height='1000' viewBox='0 0 1000 1000'>\n" \
+ " <rect x='0' y='0' width='1000' height='1000' />\n" \
+ " <g id='group' transform='translate(200 200) scale(60, 60)'>\n" \
+ " <rect id='clip' x='0' y='0' width='10' height='10' />\n" \
+ " <path id='path' stroke-width='2' fill='none' stroke='#fff'\n" \
+ " d='" path "' />\n" \
+ " </g>\n" \
+ "</svg>\n" | cmd
+ close(cmd)
+
+ print "Select(0u" code ")\n" \
+ "Import('" filename "')" | FontForge
+}
+
+BEGIN {
+ FontForge = "fontforge -lang=ff -"
+ print "New()" | FontForge
+
+ # Designed a 10x10 raster, going for maximum simplicity.
+ glyph("B", "02", "m 6,5 c 0,0 2,0 2,2 0,2 -2,2 -2,2 h -3 v -8 h 2.5 c 0,0 2,0 2,2 0,2 -2,2 -2,2 h -2 Z")
+ glyph("C", "03", "m 7.6,7 A 3,4 0 0 1 4.25,8.875 3,4 0 0 1 2,5 3,4 0 0 1 4.25,1.125 3,4 0 0 1 7.6,3")
+ glyph("I", "1D", "m 3,9 h 4 m 0,-8 h -4 m 2,-1 v 10")
+ glyph("M", "11", "m 2,10 v -10 l 3,6 3,-6 v 10")
+ glyph("O", "0F", "m 1,9 l 8,-8 M 2,5 a 3,3 0 1 0 6,0 3,3 0 1 0 -6,0 z")
+ #glyph("R", "0F", "m 3,10 v -9 h 2 c 0,0 2.5,0 2.5,2.5 0,2.5 -2.5,2.5 -2.5,2.5 h -2 2.5 l 2.5,4.5")
+ glyph("S", "1E", "m 7.5,3 c 0,-1 -1,-2 -2.5,-2 -1.5,0 -2.5,1 -2.5,2 0,3 5,1 5,4 0,1 -1,2 -2.5,2 -1.5,0 -2.5,-1 -2.5,-2")
+ glyph("U", "1F", "m 2.5,0 v 6.5 c 0,1.5 1,2.5 2.5,2.5 1.5,0 2.5,-1 2.5,-2.5 v -6.5")
+ glyph("V", "16", "m 2,-1 3,11 3,-11")
+
+ # In practice, your typical browser font will overshoot its em box,
+ # so to make the display more cohesive, we need to do the same.
+ # Sadly, sf->use_typo_metrics can't be unset from FontForge script--
+ # this is necessary to prevent the caret from jumping upon the first
+ # inserted non-formatting character in xP's textarea.
+ # https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align
+ print "SelectAll()\n" \
+ "Scale(115, 115, 0, 0)\n" \
+ "SetOS2Value('WinAscentIsOffset', 1)\n" \
+ "SetOS2Value('WinDescentIsOffset', 1)\n" \
+ "SetOS2Value('HHeadAscentIsOffset', 1)\n" \
+ "SetOS2Value('HHeadDescentIsOffset', 1)\n" \
+ "CorrectDirection()\n" \
+ "AutoWidth(100)\n" \
+ "AutoHint()\n" \
+ "AddExtrema()\n" \
+ "RoundToInt()\n" \
+ "SetFontNames('IRCFormatting-Regular'," \
+ " 'IRC Formatting', 'IRC Formatting Regular', 'Regular'," \
+ " 'Copyright (c) 2022, Premysl Eric Janouch')\n" \
+ "Generate('" Output "')\n" | FontForge
+ close(FontForge)
+}
diff --git a/xP/go.mod b/xP/go.mod
new file mode 100644
index 0000000..70cc10d
--- /dev/null
+++ b/xP/go.mod
@@ -0,0 +1,10 @@
+module janouch.name/xK/xP
+
+go 1.18
+
+require nhooyr.io/websocket v1.8.7
+
+require (
+ github.com/klauspost/compress v1.15.9 // indirect
+ golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
+)
diff --git a/xP/go.sum b/xP/go.sum
new file mode 100644
index 0000000..c94a673
--- /dev/null
+++ b/xP/go.sum
@@ -0,0 +1,62 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
+github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
diff --git a/xP/public/ircfmt.woff2 b/xP/public/ircfmt.woff2
new file mode 100644
index 0000000..d4262bc
--- /dev/null
+++ b/xP/public/ircfmt.woff2
Binary files differ
diff --git a/xP/public/xP.css b/xP/public/xP.css
new file mode 100644
index 0000000..e8b28f2
--- /dev/null
+++ b/xP/public/xP.css
@@ -0,0 +1,257 @@
+@font-face {
+ src: url('ircfmt.woff2') format('woff2');
+ font-family: 'IRC Formatting';
+ font-weight: normal;
+ font-style: normal;
+}
+body {
+ margin: 0;
+ padding: 0;
+ /* Firefox only renders C0 within the textarea, why? */
+ font-family: 'IRC Formatting', sans-serif;
+ font-size: clamp(0.5rem, 2vw, 1rem);
+}
+.xP {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ height: 100vh;
+ /* https://caniuse.com/viewport-unit-variants */
+ height: 100dvh;
+}
+
+.overlay {
+ padding: .6em .9em;
+ background: #eee;
+ border: 1px outset #eee;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ z-index: 1;
+}
+.title, .status {
+ padding: .05em .3em;
+ background: #eee;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ column-gap: .3em;
+
+ position: relative;
+ border-top: 3px solid #ccc;
+ border-bottom: 2px solid #888;
+}
+.title {
+ /* To approximate right-aligned space-between. */
+ flex-direction: row-reverse;
+}
+.title:before, .status:before {
+ content: " ";
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 2px;
+ top: -2px;
+ background: #fff;
+}
+.title:after, .status:after {
+ content: " ";
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 1px;
+ bottom: -1px;
+ background: #ccc;
+}
+
+.toolbar {
+ display: flex;
+ align-items: baseline;
+ margin-right: -.3em;
+}
+.indicator {
+ margin: 0 .3em;
+}
+button {
+ font: inherit;
+ background: transparent;
+ border: 1px solid transparent;
+ padding: 0 .3em;
+}
+button:focus {
+ border: 1px dotted #000;
+}
+button:hover {
+ border-left: 1px solid #fff;
+ border-top: 1px solid #fff;
+ border-right: 1px solid #888;
+ border-bottom: 1px solid #888;
+}
+button:hover:active {
+ border-left: 1px solid #888;
+ border-top: 1px solid #888;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+}
+
+.middle {
+ flex: auto;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+}
+
+.list {
+ overflow-y: auto;
+ border-right: 2px solid #ccc;
+ min-width: 10em;
+ flex-shrink: 0;
+}
+.item {
+ padding: .05em .3em;
+ cursor: default;
+}
+.item.highlighted {
+ color: #ff5f00;
+}
+.item.activity {
+ font-weight: bold;
+}
+.item.current {
+ font-style: italic;
+ background: #eee;
+}
+
+/* Only Firefox currently supports align-content: safe end, thus this. */
+.buffer-container {
+ flex: auto;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+}
+.filler {
+ flex: auto;
+}
+.buffer {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ overflow-y: auto;
+}
+.log {
+ font-family: monospace;
+ overflow-y: auto;
+}
+.log, .content, .completions {
+ /* Note: https://bugs.chromium.org/p/chromium/issues/detail?id=1261435 */
+ white-space: break-spaces;
+ overflow-wrap: break-word;
+}
+.log, .buffer .content {
+ padding: .1em .3em;
+}
+
+.leaked {
+ opacity: 50%;
+}
+.date {
+ padding: .3em;
+ grid-column: span 2;
+ font-weight: bold;
+}
+.unread {
+ grid-column: span 2;
+ border-top: 1px solid #ff5f00;
+}
+.time {
+ padding: .1em .3em;
+ background: #f8f8f8;
+ color: #bbb;
+ border-right: 1px solid #ccc;
+}
+.time.hidden:after {
+ border-top: .2em dotted #ccc;
+ display: block;
+ width: 50%;
+ margin: 0 auto;
+ content: "";
+}
+.mark {
+ padding-right: .3em;
+ text-align: center;
+ display: inline-block;
+ min-width: 2em;
+}
+.mark.error {
+ color: red;
+}
+.mark.join {
+ color: green;
+}
+.mark.part {
+ color: red;
+}
+.mark.action {
+ color: darkred;
+}
+.content .b {
+ font-weight: bold;
+}
+.content .i {
+ font-style: italic;
+}
+.content .u {
+ text-decoration: underline;
+}
+.content .s {
+ text-decoration: line-through;
+}
+.content .m {
+ font-family: monospace;
+}
+
+.completions {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: .05em .3em;
+ border-top: 1px solid #888;
+
+ max-height: 50%;
+ display: flex;
+ flex-flow: column wrap;
+ column-gap: .6em;
+ overflow-x: auto;
+}
+.input {
+ flex-shrink: 0;
+ border: 2px inset #eee;
+ overflow: hidden;
+ resize: vertical;
+ display: flex;
+}
+.input:focus-within {
+ border-color: #ff5f00;
+}
+.prompt {
+ padding: .05em .3em;
+ border-right: 1px solid #ccc;
+ background: #f8f8f8;
+ font-weight: bold;
+}
+textarea {
+ font: inherit;
+ padding: .05em .3em;
+ margin: 0;
+ border: 0;
+ flex-grow: 1;
+ resize: none;
+}
+textarea:focus {
+ outline: none;
+}
diff --git a/xP/public/xP.js b/xP/public/xP.js
new file mode 100644
index 0000000..3266063
--- /dev/null
+++ b/xP/public/xP.js
@@ -0,0 +1,1108 @@
+// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+// SPDX-License-Identifier: 0BSD
+import * as Relay from './proto.js'
+
+// ---- RPC --------------------------------------------------------------------
+
+class RelayRPC extends EventTarget {
+ constructor(url) {
+ super()
+ this.url = url
+ this.commandSeq = 0
+ }
+
+ connect() {
+ // We can't close the connection immediately, as that queues a task.
+ if (this.ws !== undefined)
+ throw "Already connecting or connected"
+
+ return new Promise((resolve, reject) => {
+ let ws = this.ws = new WebSocket(this.url)
+ ws.onopen = event => {
+ this._initialize()
+ resolve()
+ }
+ // It's going to be code 1006 with no further info.
+ ws.onclose = event => {
+ this.ws = undefined
+ reject()
+ }
+ })
+ }
+
+ _initialize() {
+ this.ws.binaryType = 'arraybuffer'
+ this.ws.onopen = undefined
+ this.ws.onmessage = event => {
+ this._process(event.data)
+ }
+ this.ws.onerror = event => {
+ this.dispatchEvent(new CustomEvent('error'))
+ }
+ this.ws.onclose = event => {
+ let message = "Connection closed: " +
+ event.reason + " (" + event.code + ")"
+ for (const seq in this.promised)
+ this.promised[seq].reject(message)
+
+ this.ws = undefined
+ this.dispatchEvent(new CustomEvent('close', {
+ detail: {message, code: event.code, reason: event.reason},
+ }))
+
+ // Now connect() can be called again.
+ }
+
+ this.promised = {}
+ }
+
+ _process(data) {
+ if (typeof data === 'string')
+ throw "JSON messages not supported"
+
+ const r = new Relay.Reader(data)
+ while (!r.empty)
+ this._processOne(Relay.EventMessage.deserialize(r))
+ }
+
+ _processOne(message) {
+ let e = message.data
+ switch (e.event) {
+ case Relay.Event.Error:
+ if (this.promised[e.commandSeq] !== undefined)
+ this.promised[e.commandSeq].reject(e.error)
+ else
+ console.error(`Unawaited error: ${e.error}`)
+ break
+ case Relay.Event.Response:
+ if (this.promised[e.commandSeq] !== undefined)
+ this.promised[e.commandSeq].resolve(e.data)
+ else
+ console.error("Unawaited response")
+ break
+ default:
+ e.eventSeq = message.eventSeq
+ this.dispatchEvent(new CustomEvent('event', {detail: e}))
+ return
+ }
+
+ delete this.promised[e.commandSeq]
+ for (const seq in this.promised) {
+ // We don't particularly care about wraparound issues.
+ if (seq >= e.commandSeq)
+ continue
+
+ this.promised[seq].reject("No response")
+ delete this.promised[seq]
+ }
+ }
+
+ send(params) {
+ if (this.ws === undefined)
+ throw "Not connected"
+ if (typeof params !== 'object')
+ throw "Method parameters must be an object"
+
+ // Left shifts in Javascript convert to a 32-bit signed number.
+ let seq = ++this.commandSeq
+ if ((seq << 0) != seq)
+ seq = this.commandSeq = 0
+
+ this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
+
+ // Automagically detect if we want a result.
+ let data = undefined
+ const promise = new Promise(
+ (resolve, reject) => { data = {resolve, reject} })
+ promise.then = (...args) => {
+ this.promised[seq] = data
+ return Promise.prototype.then.call(promise, ...args)
+ }
+ return promise
+ }
+}
+
+// ---- Utilities --------------------------------------------------------------
+
+function utf8Encode(s) { return new TextEncoder().encode(s) }
+function utf8Decode(s) { return new TextDecoder().decode(s) }
+
+function hasShortcutModifiers(event) {
+ return (event.altKey || event.escapePrefix) &&
+ !event.metaKey && !event.ctrlKey
+}
+
+const audioContext = new AudioContext()
+
+function beep() {
+ let gain = audioContext.createGain()
+ gain.gain.value = 0.5
+ gain.connect(audioContext.destination)
+
+ let oscillator = audioContext.createOscillator()
+ oscillator.type = "triangle"
+ oscillator.frequency.value = 800
+ oscillator.connect(gain)
+ oscillator.start(audioContext.currentTime)
+ oscillator.stop(audioContext.currentTime + 0.1)
+}
+
+let iconLink = undefined
+let iconState = undefined
+
+function updateIcon(highlighted) {
+ if (iconState === highlighted)
+ return
+
+ iconState = highlighted
+ let canvas = document.createElement('canvas')
+ canvas.width = 32
+ canvas.height = 32
+
+ let ctx = canvas.getContext('2d')
+ ctx.arc(16, 16, 12, 0, 2 * Math.PI)
+ ctx.fillStyle = '#000'
+ if (highlighted === true)
+ ctx.fillStyle = '#ff5f00'
+ if (highlighted === false)
+ ctx.fillStyle = '#ccc'
+ ctx.fill()
+
+ if (iconLink === undefined) {
+ iconLink = document.createElement('link')
+ iconLink.type = 'image/png'
+ iconLink.rel = 'icon'
+ document.getElementsByTagName('head')[0].appendChild(iconLink)
+ }
+
+ iconLink.href = canvas.toDataURL();
+}
+
+// ---- Event processing -------------------------------------------------------
+
+let rpc = new RelayRPC(proxy)
+let rpcEventHandlers = new Map()
+
+let buffers = new Map()
+let bufferLast = undefined
+let bufferCurrent = undefined
+let bufferLog = undefined
+let bufferAutoscroll = true
+
+let servers = new Map()
+
+function bufferResetStats(b) {
+ b.newMessages = 0
+ b.newUnimportantMessages = 0
+ b.highlighted = false
+}
+
+function bufferActivate(name) {
+ rpc.send({command: 'BufferActivate', bufferName: name})
+}
+
+function bufferToggleUnimportant(name) {
+ rpc.send({command: 'BufferToggleUnimportant', bufferName: name})
+}
+
+function bufferToggleLog() {
+ // TODO: Try to restore the previous scroll offset.
+ if (bufferLog) {
+ setTimeout(() =>
+ document.getElementById('input')?.focus())
+
+ bufferLog = undefined
+ m.redraw()
+ return
+ }
+
+ let name = bufferCurrent
+ rpc.send({
+ command: 'BufferLog',
+ bufferName: name,
+ }).then(resp => {
+ if (bufferCurrent !== name)
+ return
+
+ bufferLog = utf8Decode(resp.log)
+ m.redraw()
+ })
+}
+
+let connecting = true
+rpc.connect().then(result => {
+ buffers.clear()
+ bufferLast = undefined
+ bufferCurrent = undefined
+ bufferLog = undefined
+ bufferAutoscroll = true
+
+ servers.clear()
+
+ rpc.send({command: 'Hello', version: Relay.version})
+ connecting = false
+ m.redraw()
+}).catch(error => {
+ connecting = false
+ m.redraw()
+})
+
+rpc.addEventListener('close', event => {
+ m.redraw()
+})
+
+rpc.addEventListener('event', event => {
+ const handler = rpcEventHandlers.get(event.detail.event)
+ if (handler !== undefined) {
+ handler(event.detail)
+ if (bufferCurrent !== undefined ||
+ event.detail.event !== Relay.Event.BufferLine)
+ m.redraw()
+ }
+})
+
+rpcEventHandlers.set(Relay.Event.Ping, e => {
+ rpc.send({command: 'PingResponse', eventSeq: e.eventSeq})
+})
+
+// ~~~ Buffer events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+rpcEventHandlers.set(Relay.Event.BufferLine, e => {
+ let b = buffers.get(e.bufferName), line = {...e}
+ delete line.event
+ delete line.eventSeq
+ delete line.leakToActive
+ if (b === undefined)
+ return
+
+ // Initial sync: skip all other processing, let highlights be.
+ if (bufferCurrent === undefined) {
+ b.lines.push(line)
+ return
+ }
+
+ let visible = document.visibilityState !== 'hidden' &&
+ bufferLog === undefined &&
+ bufferAutoscroll &&
+ (e.bufferName == bufferCurrent || e.leakToActive)
+ b.lines.push({...line})
+ if (!(visible || e.leakToActive) ||
+ b.newMessages || b.newUnimportantMessages) {
+ if (line.isUnimportant)
+ b.newUnimportantMessages++
+ else
+ b.newMessages++
+ }
+
+ if (e.leakToActive) {
+ let bc = buffers.get(bufferCurrent)
+ bc.lines.push({...line, leaked: true})
+ if (!visible || bc.newMessages || bc.newUnimportantMessages) {
+ if (line.isUnimportant)
+ bc.newUnimportantMessages++
+ else
+ bc.newMessages++
+ }
+ }
+
+ if (line.isHighlight || (!visible && !line.isUnimportant &&
+ b.kind === Relay.BufferKind.PrivateMessage)) {
+ beep()
+ if (!visible)
+ b.highlighted = true
+ }
+})
+
+rpcEventHandlers.set(Relay.Event.BufferUpdate, e => {
+ let b = buffers.get(e.bufferName)
+ if (b === undefined) {
+ buffers.set(e.bufferName, (b = {
+ lines: [],
+ history: [],
+ historyAt: 0,
+ }))
+ bufferResetStats(b)
+ }
+
+ b.hideUnimportant = e.hideUnimportant
+ b.kind = e.context.kind
+ b.server = servers.get(e.context.serverName)
+ b.topic = e.context.topic
+ b.modes = e.context.modes
+})
+
+rpcEventHandlers.set(Relay.Event.BufferStats, e => {
+ let b = buffers.get(e.bufferName)
+ if (b === undefined)
+ return
+
+ b.newMessages = e.newMessages,
+ b.newUnimportantMessages = e.newUnimportantMessages
+ b.highlighted = e.highlighted
+})
+
+rpcEventHandlers.set(Relay.Event.BufferRename, e => {
+ buffers.set(e.new, buffers.get(e.bufferName))
+ buffers.delete(e.bufferName)
+})
+
+rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
+ buffers.delete(e.bufferName)
+ if (e.bufferName === bufferLast)
+ bufferLast = undefined
+})
+
+rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
+ let old = buffers.get(bufferCurrent)
+ if (old !== undefined)
+ bufferResetStats(old)
+
+ bufferLast = bufferCurrent
+ let b = buffers.get(e.bufferName)
+ bufferCurrent = e.bufferName
+ bufferLog = undefined
+ bufferAutoscroll = true
+ if (b !== undefined && document.visibilityState !== 'hidden')
+ b.highlighted = false
+
+ let textarea = document.getElementById('input')
+ if (textarea === null)
+ return
+
+ textarea.focus()
+ if (old !== undefined) {
+ old.input = textarea.value
+ old.inputStart = textarea.selectionStart
+ old.inputEnd = textarea.selectionEnd
+ old.inputDirection = textarea.selectionDirection
+ // Note that we effectively overwrite the newest line
+ // with the current textarea contents, and jump there.
+ old.historyAt = old.history.length
+ }
+
+ textarea.value = ''
+ if (b !== undefined && b.input !== undefined) {
+ textarea.value = b.input
+ textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection)
+ }
+})
+
+rpcEventHandlers.set(Relay.Event.BufferClear, e => {
+ let b = buffers.get(e.bufferName)
+ if (b !== undefined)
+ b.lines.length = 0
+})
+
+// ~~~ Server events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+rpcEventHandlers.set(Relay.Event.ServerUpdate, e => {
+ let s = servers.get(e.serverName)
+ if (s === undefined)
+ servers.set(e.serverName, (s = {}))
+ s.data = e.data
+})
+
+rpcEventHandlers.set(Relay.Event.ServerRename, e => {
+ servers.set(e.new, servers.get(e.serverName))
+ servers.delete(e.serverName)
+})
+
+rpcEventHandlers.set(Relay.Event.ServerRemove, e => {
+ servers.delete(e.serverName)
+})
+
+// --- Colours -----------------------------------------------------------------
+
+let palette = [
+ '#000', '#800', '#080', '#880', '#008', '#808', '#088', '#ccc',
+ '#888', '#f00', '#0f0', '#ff0', '#00f', '#f0f', '#0ff', '#fff',
+]
+palette.length = 256
+for (let i = 0; i < 216; i++) {
+ let r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6
+ r = !r ? '00' : (55 + 40 * r).toString(16)
+ g = !g ? '00' : (55 + 40 * g).toString(16)
+ b = !b ? '00' : (55 + 40 * b).toString(16)
+ palette[16 + i] = `#${r}${g}${b}`
+}
+for (let i = 0; i < 24; i++) {
+ let g = ('0' + (8 + i * 10).toString(16)).slice(-2)
+ palette[232 + i] = `#${g}${g}${g}`
+}
+
+// ---- UI ---------------------------------------------------------------------
+
+let linkRE = [
+ /https?:\/\//,
+ /([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
+ /([^\[\](){}<>"'\s,.:]|\([^\[\](){}<>"'\s]*\))/,
+].map(r => r.source).join('')
+
+let BufferList = {
+ view: vnode => {
+ let highlighted = false
+ let items = Array.from(buffers, ([name, b]) => {
+ let classes = [], displayName = name
+ if (name == bufferCurrent) {
+ classes.push('current')
+ } else if (b.newMessages) {
+ classes.push('activity')
+ displayName += ` (${b.newMessages})`
+ }
+ if (b.highlighted) {
+ classes.push('highlighted')
+ highlighted = true
+ }
+ return m('.item', {
+ onclick: event => bufferActivate(name),
+ class: classes.join(' '),
+ }, displayName)
+ })
+
+ updateIcon(rpc.ws === undefined ? null : highlighted)
+ return m('.list', {}, items)
+ },
+}
+
+let Content = {
+ applyColor: (fg, bg, inverse) => {
+ if (inverse)
+ [fg, bg] = [bg >= 0 ? bg : 15, fg >= 0 ? fg : 0]
+
+ let style = {}
+ if (fg >= 0)
+ style.color = palette[fg]
+ if (bg >= 0)
+ style.backgroundColor = palette[bg]
+ for (const _ in style)
+ return style
+ },
+
+ linkify: (text, attrs) => {
+ let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
+ while ((match = re.exec(text)) !== null) {
+ if (end < match.index)
+ a.push(m('span', attrs, text.substring(end, match.index)))
+ a.push(m('a[target=_blank]', {href: match[0], ...attrs}, match[0]))
+ end = re.lastIndex
+ }
+ if (end < text.length)
+ a.push(m('span', attrs, text.substring(end)))
+ return a
+ },
+
+ makeMark: line => {
+ switch (line.rendition) {
+ case Relay.Rendition.Indent: return m('span.mark', {}, '')
+ case Relay.Rendition.Status: return m('span.mark', {}, '–')
+ case Relay.Rendition.Error: return m('span.mark.error', {}, '⚠')
+ case Relay.Rendition.Join: return m('span.mark.join', {}, '→')
+ case Relay.Rendition.Part: return m('span.mark.part', {}, '←')
+ case Relay.Rendition.Action: return m('span.mark.action', {}, '✶')
+ }
+ },
+
+ view: vnode => {
+ let line = vnode.children[0]
+ let classes = new Set()
+ let flip = c => {
+ if (classes.has(c))
+ classes.delete(c)
+ else
+ classes.add(c)
+ }
+
+ let fg = -1, bg = -1, inverse = false
+ return m('.content', vnode.attrs, [
+ Content.makeMark(line),
+ line.items.flatMap(item => {
+ switch (item.kind) {
+ case Relay.Item.Text:
+ return Content.linkify(item.text, {
+ class: Array.from(classes.keys()).join(' '),
+ style: Content.applyColor(fg, bg, inverse),
+ })
+ case Relay.Item.Reset:
+ classes.clear()
+ fg = bg = -1
+ inverse = false
+ break
+ case Relay.Item.FgColor:
+ fg = item.color
+ break
+ case Relay.Item.BgColor:
+ bg = item.color
+ break
+ case Relay.Item.FlipInverse:
+ inverse = !inverse
+ break
+ case Relay.Item.FlipBold:
+ flip('b')
+ break
+ case Relay.Item.FlipItalic:
+ flip('i')
+ break
+ case Relay.Item.FlipUnderline:
+ flip('u')
+ break
+ case Relay.Item.FlipCrossedOut:
+ flip('s')
+ break
+ case Relay.Item.FlipMonospace:
+ flip('m')
+ break
+ }
+ }),
+ ])
+ },
+}
+
+let Topic = {
+ view: vnode => {
+ let b = buffers.get(bufferCurrent)
+ if (b !== undefined && b.topic !== undefined)
+ return m(Content, {}, {items: b.topic})
+ },
+}
+
+let Buffer = {
+ controller: new AbortController(),
+
+ onbeforeremove: vnode => {
+ Buffer.controller.abort()
+ },
+
+ onupdate: vnode => {
+ if (bufferAutoscroll)
+ vnode.dom.scrollTop = vnode.dom.scrollHeight
+ },
+
+ oncreate: vnode => {
+ Buffer.onupdate(vnode)
+ window.addEventListener('resize', event => Buffer.onupdate(vnode),
+ {signal: Buffer.controller.signal})
+ },
+
+ view: vnode => {
+ let lines = []
+ let b = buffers.get(bufferCurrent)
+ if (b === undefined)
+ return m('.buffer')
+
+ let lastDateMark = undefined
+ let squashing = false
+ let markBefore = b.lines.length
+ - b.newMessages - b.newUnimportantMessages
+ b.lines.forEach((line, i) => {
+ if (i == markBefore)
+ lines.push(m('.unread'))
+
+ if (!line.isUnimportant || !b.hideUnimportant) {
+ squashing = false
+ } else if (squashing) {
+ return
+ } else {
+ squashing = true
+ }
+
+ let date = new Date(line.when)
+ let dateMark = date.toLocaleDateString()
+ if (dateMark !== lastDateMark) {
+ lines.push(m('.date', {}, dateMark))
+ lastDateMark = dateMark
+ }
+ if (squashing) {
+ lines.push(m('.time.hidden'))
+ lines.push(m('.content'))
+ return
+ }
+
+ let attrs = {}
+ if (line.leaked)
+ attrs.class = 'leaked'
+
+ lines.push(m('.time', {...attrs}, date.toLocaleTimeString()))
+ lines.push(m(Content, {...attrs}, line))
+ })
+
+ let dateMark = new Date().toLocaleDateString()
+ if (dateMark !== lastDateMark && lastDateMark !== undefined)
+ lines.push(m('.date', {}, dateMark))
+ return m('.buffer', {onscroll: event => {
+ const dom = event.target
+ bufferAutoscroll =
+ dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
+ }}, lines)
+ },
+}
+
+let Log = {
+ oncreate: vnode => {
+ vnode.dom.scrollTop = vnode.dom.scrollHeight
+ vnode.dom.focus()
+ },
+
+ linkify: text => {
+ let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
+ while ((match = re.exec(text)) !== null) {
+ if (end < match.index)
+ a.push(text.substring(end, match.index))
+ a.push(m('a[target=_blank]', {href: match[0]}, match[0]))
+ end = re.lastIndex
+ }
+ if (end < text.length)
+ a.push(text.substring(end))
+ return a
+ },
+
+ view: vnode => {
+ return m(".log", {}, Log.linkify(bufferLog))
+ },
+}
+
+let Completions = {
+ entries: [],
+
+ reset: list => {
+ Completions.entries = list || []
+ m.redraw()
+ },
+
+ view: vnode => {
+ if (!Completions.entries.length)
+ return
+ return m('.completions', {},
+ Completions.entries.map(option => m('.completion', {}, option)))
+ },
+}
+
+let BufferContainer = {
+ view: vnode => {
+ return m('.buffer-container', {}, [
+ m('.filler'),
+ bufferLog !== undefined ? m(Log) : m(Buffer),
+ m(Completions),
+ ])
+ },
+}
+
+let Toolbar = {
+ format: formatting => {
+ let textarea = document.getElementById('input')
+ if (textarea !== null)
+ Input.format(textarea, formatting)
+ },
+
+ view: vnode => {
+ let indicators = []
+ if (bufferLog === undefined && !bufferAutoscroll)
+ indicators.push(m('.indicator', {}, '⇩'))
+ if (Input.formatting)
+ indicators.push(m('.indicator', {}, '#'))
+ return m('.toolbar', {}, [
+ indicators,
+ m('button', {onclick: event => Toolbar.format('\u0002')},
+ m('b', {}, 'B')),
+ m('button', {onclick: event => Toolbar.format('\u001D')},
+ m('i', {}, 'I')),
+ m('button', {onclick: event => Toolbar.format('\u001F')},
+ m('u', {}, 'U')),
+ m('button', {onclick: event => bufferToggleLog()},
+ bufferLog === undefined ? 'Log' : 'Hide log'),
+ ])
+ },
+}
+
+let Status = {
+ view: vnode => {
+ let b = buffers.get(bufferCurrent)
+ if (b === undefined)
+ return m('.status', {}, 'Synchronizing...')
+
+ let status = `${bufferCurrent}`
+ if (b.modes)
+ status += `(+${b.modes})`
+ if (b.hideUnimportant)
+ status += `<H>`
+ return m('.status', {}, [status, m(Toolbar)])
+ },
+}
+
+let Prompt = {
+ view: vnode => {
+ let b = buffers.get(bufferCurrent)
+ if (b === undefined || b.server === undefined)
+ return
+
+ if (b.server.data.user !== undefined) {
+ let user = b.server.data.user
+ if (b.server.data.userModes)
+ user += `(${b.server.data.userModes})`
+ return m('.prompt', {}, `${user}`)
+ }
+
+ // This might certainly be done more systematically.
+ let state = b.server.data.state
+ for (const s in Relay.ServerState)
+ if (Relay.ServerState[s] == state) {
+ state = s
+ break
+ }
+ return m('.prompt', {}, `(${state})`)
+ },
+}
+
+let Input = {
+ counter: 0,
+ stamp: textarea => {
+ return [Input.counter,
+ textarea.selectionStart, textarea.selectionEnd, textarea.value]
+ },
+
+ complete: (b, textarea) => {
+ if (textarea.selectionStart !== textarea.selectionEnd)
+ return false
+
+ // Cancel any previous autocomplete, and ensure applicability.
+ Input.counter++
+ let state = Input.stamp(textarea)
+ rpc.send({
+ command: 'BufferComplete',
+ bufferName: bufferCurrent,
+ text: textarea.value,
+ position: utf8Encode(
+ textarea.value.slice(0, textarea.selectionEnd)).length,
+ }).then(resp => {
+ if (!Input.stamp(textarea).every((v, k) => v === state[k]))
+ return
+
+ let preceding = utf8Encode(textarea.value).slice(0, resp.start)
+ let start = utf8Decode(preceding).length
+ if (resp.completions.length > 0) {
+ textarea.setRangeText(resp.completions[0],
+ start, textarea.selectionEnd, 'end')
+ }
+
+ if (resp.completions.length == 1) {
+ textarea.setRangeText(' ',
+ textarea.selectionStart, textarea.selectionEnd, 'end')
+ } else {
+ beep()
+ }
+
+ if (resp.completions.length > 1)
+ Completions.reset(resp.completions.slice(1))
+ })
+ return true
+ },
+
+ submit: (b, textarea) => {
+ rpc.send({
+ command: 'BufferInput',
+ bufferName: bufferCurrent,
+ text: textarea.value,
+ })
+
+ // b.history[b.history.length] is virtual, and is represented
+ // either by textarea contents when it's currently being edited,
+ // or by b.input in all other cases.
+ b.history.push(textarea.value)
+ b.historyAt = b.history.length
+ textarea.value = ''
+ return true
+ },
+
+ backward: textarea => {
+ if (textarea.selectionStart !== textarea.selectionEnd)
+ return false
+
+ let point = textarea.selectionStart
+ if (point < 1)
+ return false
+ while (point && /\s/.test(textarea.value.charAt(--point))) {}
+ while (point-- && !/\s/.test(textarea.value.charAt(point))) {}
+ point++
+ textarea.setSelectionRange(point, point)
+ return true
+ },
+
+ forward: textarea => {
+ if (textarea.selectionStart !== textarea.selectionEnd)
+ return false
+
+ let point = textarea.selectionStart, len = textarea.value.length
+ if (point + 1 > len)
+ return false
+ while (point < len && /\s/.test(textarea.value.charAt(point))) point++
+ while (point < len && !/\s/.test(textarea.value.charAt(point))) point++
+ textarea.setSelectionRange(point, point)
+ return true
+ },
+
+ modifyWord: (textarea, cb) => {
+ let start = textarea.selectionStart
+ let end = textarea.selectionEnd
+ if (start === end) {
+ let len = textarea.value.length
+ while (start < len && /\s/.test(textarea.value.charAt(start)))
+ start++;
+ end = start
+ while (end < len && !/\s/.test(textarea.value.charAt(end)))
+ end++;
+ }
+ if (start === end)
+ return false
+
+ const text = textarea.value, modified = cb(text.substring(start, end))
+ textarea.value = text.slice(0, start) + modified + text.slice(end)
+ end = start + modified.length
+ textarea.setSelectionRange(end, end)
+ return true
+ },
+
+ downcase: textarea => {
+ return Input.modifyWord(textarea, text => text.toLowerCase())
+ },
+
+ upcase: textarea => {
+ return Input.modifyWord(textarea, text => text.toUpperCase())
+ },
+
+ capitalize: textarea => {
+ return Input.modifyWord(textarea, text => {
+ const cps = Array.from(text.toLowerCase())
+ return cps[0].toUpperCase() + cps.slice(1).join('')
+ })
+ },
+
+ first: (b, textarea) => {
+ if (b.historyAt <= 0)
+ return false
+
+ if (b.historyAt == b.history.length)
+ b.input = textarea.value
+ textarea.value = b.history[(b.historyAt = 0)]
+ return true
+ },
+
+ last: (b, textarea) => {
+ if (b.historyAt >= b.history.length)
+ return false
+
+ b.historyAt = b.history.length
+ textarea.value = b.input
+ return true
+ },
+
+ previous: (b, textarea) => {
+ if (b.historyAt <= 0)
+ return false
+
+ if (b.historyAt == b.history.length)
+ b.input = textarea.value
+ textarea.value = b.history[--b.historyAt]
+ return true
+ },
+
+ next: (b, textarea) => {
+ if (b.historyAt >= b.history.length)
+ return false
+
+ if (++b.historyAt == b.history.length)
+ textarea.value = b.input
+ else
+ textarea.value = b.history[b.historyAt]
+ return true
+ },
+
+ formatting: false,
+
+ format: (textarea, formatting) => {
+ const [start, end] = [textarea.selectionStart, textarea.selectionEnd]
+ if (start === end) {
+ textarea.setRangeText(formatting)
+ textarea.setSelectionRange(
+ start + formatting.length, end + formatting.length)
+ } else {
+ textarea.setRangeText(
+ formatting + textarea.value.substr(start, end) + formatting)
+ }
+ textarea.focus()
+ },
+
+ onKeyDown: event => {
+ // TODO: And perhaps on other actions, too.
+ rpc.send({command: 'Active'})
+
+ let b = buffers.get(bufferCurrent)
+ if (b === undefined)
+ return
+
+ let textarea = event.currentTarget
+ let handled = false
+ let success = true
+ if (Input.formatting) {
+ Input.formatting = false
+
+ // Like process_formatting_escape() within xC.
+ handled = true
+ switch (event.key) {
+ case 'b': Input.format(textarea, '\u0002'); break
+ case 'c': Input.format(textarea, '\u0003'); break
+ case 'q':
+ case 'm': Input.format(textarea, '\u0011'); break
+ case 'v': Input.format(textarea, '\u0016'); break
+ case 'i':
+ case ']': Input.format(textarea, '\u001D'); break
+ case 's':
+ case 'x':
+ case '^': Input.format(textarea, '\u001E'); break
+ case 'u':
+ case '_': Input.format(textarea, '\u001F'); break
+ case 'r':
+ case 'o': Input.format(textarea, '\u000F'); break
+ default: success = false
+ }
+ } else if (hasShortcutModifiers(event)) {
+ handled = true
+ switch (event.key) {
+ case 'b': success = Input.backward(textarea); break
+ case 'f': success = Input.forward(textarea); break
+ case 'l': success = Input.downcase(textarea); break
+ case 'u': success = Input.upcase(textarea); break
+ case 'c': success = Input.capitalize(textarea); break
+ case '<': success = Input.first(b, textarea); break
+ case '>': success = Input.last(b, textarea); break
+ case 'p': success = Input.previous(b, textarea); break
+ case 'n': success = Input.next(b, textarea); break
+ case 'm': success = Input.formatting = true; break
+ default: handled = false
+ }
+ } else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
+ !event.shiftKey) {
+ handled = true
+ switch (event.keyCode) {
+ case 9: success = Input.complete(b, textarea); break
+ case 13: success = Input.submit(b, textarea); break
+ default: handled = false
+ }
+ }
+ if (!success)
+ beep()
+ if (handled)
+ event.preventDefault()
+ },
+
+ onStateChange: event => {
+ Completions.reset()
+ Input.formatting = false
+ },
+
+ view: vnode => {
+ return m('textarea#input', {
+ rows: 1,
+ onkeydown: Input.onKeyDown,
+ oninput: Input.onStateChange,
+ // Sadly only supported in Firefox as of writing.
+ onselectionchange: Input.onStateChange,
+ // The list of completions is scrollable without receiving focus.
+ onblur: Input.onStateChange,
+ })
+ },
+}
+
+let Main = {
+ view: vnode => {
+ let overlay = undefined
+ if (connecting)
+ overlay = m('.overlay', {}, "Connecting...")
+ else if (rpc.ws === undefined)
+ overlay = m('.overlay', {}, [
+ m('', {}, "Disconnected"),
+ m('', {}, m('small', {}, "Reload page to reconnect.")),
+ ])
+
+ return m('.xP', {}, [
+ overlay,
+ m('.title', {}, [m('b', {}, `xP`), m(Topic)]),
+ m('.middle', {}, [m(BufferList), m(BufferContainer)]),
+ m(Status),
+ m('.input', {}, [m(Prompt), m(Input)]),
+ ])
+ },
+}
+
+window.addEventListener('load', () => m.mount(document.body, Main))
+
+document.addEventListener('visibilitychange', event => {
+ let b = buffers.get(bufferCurrent)
+ if (b !== undefined && document.visibilityState !== 'hidden') {
+ b.highlighted = false
+ m.redraw()
+ }
+})
+
+// On macOS, the Alt/Option key transforms characters, which basically breaks
+// all event.altKey shortcuts, so implement Escape prefixing on that system.
+// This method of detection only works with Blink browsers, as of writing.
+let lastWasEscape = false
+document.addEventListener('keydown', event => {
+ event.escapePrefix = lastWasEscape
+ if (lastWasEscape) {
+ lastWasEscape = false
+ } else if (event.code == 'Escape' &&
+ navigator.userAgentData?.platform === 'macOS') {
+ event.preventDefault()
+ event.stopPropagation()
+ lastWasEscape = true
+ return
+ }
+
+ if (rpc.ws == undefined || !hasShortcutModifiers(event))
+ return
+
+ // Rotate names so that the current buffer comes first.
+ let names = [...buffers.keys()]
+ names.push.apply(names,
+ names.splice(0, names.findIndex(name => name == bufferCurrent)))
+
+ switch (event.key) {
+ case 'h':
+ bufferToggleLog()
+ break
+ case 'H':
+ if (bufferCurrent !== undefined)
+ bufferToggleUnimportant(bufferCurrent)
+ break
+ case 'a':
+ for (const name of names.slice(1))
+ if (buffers.get(name).newMessages) {
+ bufferActivate(name)
+ break
+ }
+ break
+ case '!':
+ for (const name of names.slice(1))
+ if (buffers.get(name).highlighted) {
+ bufferActivate(name)
+ break
+ }
+ break
+ case 'Tab':
+ if (bufferLast !== undefined)
+ bufferActivate(bufferLast)
+ break
+ case 'PageUp':
+ if (names.length > 1)
+ bufferActivate(names.at(-1))
+ break
+ case 'PageDown':
+ if (names.length > 1)
+ bufferActivate(names.at(+1))
+ break
+ default:
+ return
+ }
+
+ event.preventDefault()
+}, true)
diff --git a/xP/xP.go b/xP/xP.go
new file mode 100644
index 0000000..20117b2
--- /dev/null
+++ b/xP/xP.go
@@ -0,0 +1,299 @@
+// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+// SPDX-License-Identifier: 0BSD
+
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "time"
+
+ "nhooyr.io/websocket"
+)
+
+var (
+ debug = flag.Bool("debug", false, "enable debug output")
+
+ addressBind string
+ addressConnect string
+ addressWS string
+)
+
+// -----------------------------------------------------------------------------
+
+func relayReadFrame(r io.Reader) []byte {
+ var length uint32
+ if err := binary.Read(r, binary.BigEndian, &length); err != nil {
+ log.Println("Event receive failed: " + err.Error())
+ return nil
+ }
+ b := make([]byte, length)
+ if _, err := io.ReadFull(r, b); err != nil {
+ log.Println("Event receive failed: " + err.Error())
+ return nil
+ }
+
+ if *debug {
+ log.Printf("<? %v\n", b)
+
+ var m RelayEventMessage
+ if after, ok := m.ConsumeFrom(b); !ok {
+ log.Println("Event deserialization failed")
+ return nil
+ } else if len(after) != 0 {
+ log.Println("Event deserialization failed: trailing data")
+ return nil
+ }
+
+ j, err := m.MarshalJSON()
+ if err != nil {
+ log.Println("Event marshalling failed: " + err.Error())
+ return nil
+ }
+
+ log.Printf("<- %s\n", j)
+ }
+ return b
+}
+
+func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte {
+ // The usual event message rarely gets above 1 kilobyte,
+ // thus this is set to buffer up at most 1 megabyte or so.
+ p := make(chan []byte, 1000)
+ r := bufio.NewReaderSize(conn, 65536)
+ go func() {
+ defer close(p)
+ for {
+ j := relayReadFrame(r)
+ if j == nil {
+ return
+ }
+ select {
+ case p <- j:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return p
+}
+
+func relayWriteJSON(conn net.Conn, j []byte) bool {
+ var m RelayCommandMessage
+ if err := json.Unmarshal(j, &m); err != nil {
+ log.Println("Command unmarshalling failed: " + err.Error())
+ return false
+ }
+
+ b, ok := m.AppendTo(make([]byte, 4))
+ if !ok {
+ log.Println("Command serialization failed")
+ return false
+ }
+ binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4))
+ if _, err := conn.Write(b); err != nil {
+ log.Println("Command send failed: " + err.Error())
+ return false
+ }
+
+ if *debug {
+ log.Printf("-> %v\n", b)
+ }
+ return true
+}
+
+// -----------------------------------------------------------------------------
+
+func clientReadJSON(ctx context.Context, ws *websocket.Conn) []byte {
+ t, j, err := ws.Read(ctx)
+ if err != nil {
+ log.Println("Command receive failed: " + err.Error())
+ return nil
+ }
+ if t != websocket.MessageText {
+ log.Println(
+ "Command receive failed: " + "binary messages are not supported")
+ return nil
+ }
+
+ if *debug {
+ log.Printf("?> %s\n", j)
+ }
+ return j
+}
+
+func clientWriteBinary(ctx context.Context, ws *websocket.Conn, b []byte) bool {
+ if err := ws.Write(ctx, websocket.MessageBinary, b); err != nil {
+ log.Println("Event send failed: " + err.Error())
+ return false
+ }
+ return true
+}
+
+func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
+ b, ok := (&RelayEventMessage{
+ EventSeq: 0,
+ Data: RelayEventData{
+ Interface: RelayEventDataError{
+ Event: RelayEventError,
+ CommandSeq: 0,
+ Error: err.Error(),
+ },
+ },
+ }).AppendTo(nil)
+ if ok {
+ log.Println("Event serialization failed")
+ return false
+ }
+ return clientWriteBinary(ctx, ws, b)
+}
+
+func handleWS(w http.ResponseWriter, r *http.Request) {
+ ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
+ InsecureSkipVerify: true,
+ // Note that Safari can be broken with compression.
+ CompressionMode: websocket.CompressionContextTakeover,
+ // This is for the payload; set higher to avoid overhead.
+ CompressionThreshold: 64 << 10,
+ })
+ if err != nil {
+ log.Println("Client rejected: " + err.Error())
+ return
+ }
+ defer ws.Close(websocket.StatusGoingAway, "Goodbye")
+
+ ctx, cancel := context.WithCancel(r.Context())
+ defer cancel()
+
+ conn, err := net.Dial("tcp", addressConnect)
+ if err != nil {
+ log.Println("Connection failed: " + err.Error())
+ clientWriteError(ctx, ws, err)
+ return
+ }
+ defer conn.Close()
+
+ // To decrease latencies, events are received and decoded in parallel
+ // to their sending, and we try to batch them together.
+ relayFrames := relayMakeReceiver(ctx, conn)
+ batchFrames := func() []byte {
+ batch, ok := <-relayFrames
+ if !ok {
+ return nil
+ }
+ Batch:
+ for {
+ select {
+ case b, ok := <-relayFrames:
+ if !ok {
+ break Batch
+ }
+ batch = append(batch, b...)
+ default:
+ break Batch
+ }
+ }
+ return batch
+ }
+
+ // We don't need to intervene, so it's just two separate pipes so far.
+ go func() {
+ defer cancel()
+ for {
+ j := clientReadJSON(ctx, ws)
+ if j == nil {
+ return
+ }
+ relayWriteJSON(conn, j)
+ }
+ }()
+ go func() {
+ defer cancel()
+ for {
+ b := batchFrames()
+ if b == nil {
+ return
+ }
+ clientWriteBinary(ctx, ws, b)
+ }
+ }()
+ <-ctx.Done()
+}
+
+// -----------------------------------------------------------------------------
+
+var staticHandler = http.FileServer(http.Dir("."))
+
+var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
+<html>
+<head>
+ <title>xP</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="xP.css" />
+</head>
+<body>
+ <script src="mithril.js">
+ </script>
+ <script>
+ let proxy = '{{ . }}'
+ </script>
+ <script type="module" src="xP.js">
+ </script>
+</body>
+</html>`))
+
+func handleDefault(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ staticHandler.ServeHTTP(w, r)
+ return
+ }
+
+ wsURI := addressWS
+ if wsURI == "" {
+ wsURI = fmt.Sprintf("ws://%s/ws", r.Host)
+ }
+ if err := page.Execute(w, wsURI); err != nil {
+ log.Println("Template execution failed: " + err.Error())
+ }
+}
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(),
+ "Usage: %s [OPTION...] BIND CONNECT [WSURI]\n\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+
+ flag.Parse()
+ if flag.NArg() < 2 || flag.NArg() > 3 {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ addressBind, addressConnect = flag.Arg(0), flag.Arg(1)
+ if flag.NArg() > 2 {
+ addressWS = flag.Arg(2)
+ }
+
+ http.Handle("/ws", http.HandlerFunc(handleWS))
+ http.Handle("/", http.HandlerFunc(handleDefault))
+
+ s := &http.Server{
+ Addr: addressBind,
+ ReadTimeout: 60 * time.Second,
+ WriteTimeout: 60 * time.Second,
+ MaxHeaderBytes: 32 << 10,
+ }
+ log.Fatal(s.ListenAndServe())
+}