From 50057d5149dda340b3b47aca4096f4a6ec66b9ee Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Fri, 6 Aug 2021 16:12:15 +0200 Subject: Come up with sillier names for the binaries I'm not entirely sure, but it looks like some people might not like jokes about the Holocaust. On a more serious note, the project has become more serious over the 7 or so years of its existence. --- CMakeLists.txt | 60 +- NEWS | 192 +- README.adoc | 78 +- degesch.adoc | 127 - degesch.c | 14473 ------------------------------- degesch.png | Bin 9139 -> 0 bytes kike-gen-replies.sh | 28 - kike-replies | 93 - kike.adoc | 53 - kike.c | 4079 --------- plugins/degesch/auto-rejoin.lua | 48 - plugins/degesch/censor.lua | 90 - plugins/degesch/fancy-prompt.lua | 105 - plugins/degesch/last-fm.lua | 178 - plugins/degesch/ping-timeout.lua | 32 - plugins/degesch/prime.lua | 68 - plugins/degesch/slack.lua | 147 - plugins/degesch/thin-cursor.lua | 27 - plugins/degesch/utm-filter.lua | 62 - plugins/xB/calc | 241 + plugins/xB/coin | 128 + plugins/xB/eval | 312 + plugins/xB/factoids | 177 + plugins/xB/pomodoro | 502 ++ plugins/xB/script | 2310 +++++ plugins/xB/seen | 160 + plugins/xB/seen-import-xC.pl | 39 + plugins/xB/youtube | 111 + plugins/xC/auto-rejoin.lua | 48 + plugins/xC/censor.lua | 90 + plugins/xC/fancy-prompt.lua | 105 + plugins/xC/last-fm.lua | 178 + plugins/xC/ping-timeout.lua | 32 + plugins/xC/prime.lua | 68 + plugins/xC/slack.lua | 147 + plugins/xC/thin-cursor.lua | 27 + plugins/xC/utm-filter.lua | 62 + plugins/zyklonb/calc | 241 - plugins/zyklonb/coin | 128 - plugins/zyklonb/eval | 312 - plugins/zyklonb/factoids | 177 - plugins/zyklonb/pomodoro | 502 -- plugins/zyklonb/script | 2310 ----- plugins/zyklonb/seen | 160 - plugins/zyklonb/seen-import-degesch.pl | 39 - plugins/zyklonb/youtube | 111 - test | 6 +- test-nick-colors | 2 +- test-static | 2 +- xB.adoc | 104 + xB.c | 2063 +++++ xC.adoc | 127 + xC.c | 14469 ++++++++++++++++++++++++++++++ xC.png | Bin 0 -> 9139 bytes xD-gen-replies.sh | 28 + xD-replies | 93 + xD.adoc | 53 + xD.c | 4079 +++++++++ zyklonb.adoc | 104 - zyklonb.c | 2063 ----- 60 files changed, 25924 insertions(+), 25926 deletions(-) delete mode 100644 degesch.adoc delete mode 100644 degesch.c delete mode 100644 degesch.png delete mode 100755 kike-gen-replies.sh delete mode 100644 kike-replies delete mode 100644 kike.adoc delete mode 100644 kike.c delete mode 100644 plugins/degesch/auto-rejoin.lua delete mode 100644 plugins/degesch/censor.lua delete mode 100644 plugins/degesch/fancy-prompt.lua delete mode 100644 plugins/degesch/last-fm.lua delete mode 100644 plugins/degesch/ping-timeout.lua delete mode 100644 plugins/degesch/prime.lua delete mode 100644 plugins/degesch/slack.lua delete mode 100644 plugins/degesch/thin-cursor.lua delete mode 100644 plugins/degesch/utm-filter.lua create mode 100755 plugins/xB/calc create mode 100755 plugins/xB/coin create mode 100755 plugins/xB/eval create mode 100755 plugins/xB/factoids create mode 100755 plugins/xB/pomodoro create mode 100755 plugins/xB/script create mode 100755 plugins/xB/seen create mode 100755 plugins/xB/seen-import-xC.pl create mode 100755 plugins/xB/youtube create mode 100644 plugins/xC/auto-rejoin.lua create mode 100644 plugins/xC/censor.lua create mode 100644 plugins/xC/fancy-prompt.lua create mode 100644 plugins/xC/last-fm.lua create mode 100644 plugins/xC/ping-timeout.lua create mode 100644 plugins/xC/prime.lua create mode 100644 plugins/xC/slack.lua create mode 100644 plugins/xC/thin-cursor.lua create mode 100644 plugins/xC/utm-filter.lua delete mode 100755 plugins/zyklonb/calc delete mode 100755 plugins/zyklonb/coin delete mode 100755 plugins/zyklonb/eval delete mode 100755 plugins/zyklonb/factoids delete mode 100755 plugins/zyklonb/pomodoro delete mode 100755 plugins/zyklonb/script delete mode 100755 plugins/zyklonb/seen delete mode 100755 plugins/zyklonb/seen-import-degesch.pl delete mode 100755 plugins/zyklonb/youtube create mode 100644 xB.adoc create mode 100644 xB.c create mode 100644 xC.adoc create mode 100644 xC.c create mode 100644 xC.png create mode 100755 xD-gen-replies.sh create mode 100644 xD-replies create mode 100644 xD.adoc create mode 100644 xD.c delete mode 100644 zyklonb.adoc delete mode 100644 zyklonb.c diff --git a/CMakeLists.txt b/CMakeLists.txt index a18ebca..9c1d39b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,9 +77,9 @@ CHECK_C_SOURCE_RUNS ("#include int main () { return iconv_open (\"UTF-8//TRANSLIT\", \"ISO-8859-1\") == (iconv_t) -1; }" ICONV_ACCEPTS_TRANSLIT) -# Dependencies for degesch +# Dependencies for xC pkg_check_modules (libffi REQUIRED libffi) -list (APPEND degesch_libraries ${libffi_LIBRARIES}) +list (APPEND xC_libraries ${libffi_LIBRARIES}) include_directories (${libffi_INCLUDE_DIRS}) link_directories (${libffi_LIBRARY_DIRS}) @@ -92,7 +92,7 @@ if (WITH_LUA) message (FATAL_ERROR "Lua library not found") endif () - list (APPEND degesch_libraries ${lua_LIBRARIES}) + list (APPEND xC_libraries ${lua_LIBRARIES}) include_directories (${lua_INCLUDE_DIRS}) link_directories (${lua_LIBRARY_DIRS}) endif () @@ -100,10 +100,10 @@ endif () find_package (Curses) pkg_check_modules (ncursesw ncursesw) if (ncursesw_FOUND) - list (APPEND degesch_libraries ${ncursesw_LIBRARIES}) + list (APPEND xC_libraries ${ncursesw_LIBRARIES}) include_directories (${ncursesw_INCLUDE_DIRS}) elseif (CURSES_FOUND) - list (APPEND degesch_libraries ${CURSES_LIBRARY}) + list (APPEND xC_libraries ${CURSES_LIBRARY}) include_directories (${CURSES_INCLUDE_DIR}) else () message (SEND_ERROR "Curses not found") @@ -115,13 +115,13 @@ elseif (WANT_READLINE) # OpenBSD's default readline is too old if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD") include_directories (${OPENBSD_LOCALBASE}/include/ereadline) - list (APPEND degesch_libraries ereadline) + list (APPEND xC_libraries ereadline) else () - list (APPEND degesch_libraries readline) + list (APPEND xC_libraries readline) endif () elseif (WANT_LIBEDIT) pkg_check_modules (libedit REQUIRED libedit) - list (APPEND degesch_libraries ${libedit_LIBRARIES}) + list (APPEND xC_libraries ${libedit_LIBRARIES}) include_directories (${libedit_INCLUDE_DIRS}) endif () @@ -135,34 +135,34 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h 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 kike-replies.c kike.msg - COMMAND ${PROJECT_SOURCE_DIR}/kike-gen-replies.sh - > kike-replies.c < ${PROJECT_SOURCE_DIR}/kike-replies - DEPENDS ${PROJECT_SOURCE_DIR}/kike-replies +add_custom_command (OUTPUT xD-replies.c xD.msg + COMMAND ${PROJECT_SOURCE_DIR}/xD-gen-replies.sh + > xD-replies.c < ${PROJECT_SOURCE_DIR}/xD-replies + DEPENDS ${PROJECT_SOURCE_DIR}/xD-replies COMMENT "Generating files from the list of server numerics") -add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/kike-replies.c) +add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c) # Build -foreach (name zyklonb degesch kike) +foreach (name xB xC xD) add_executable (${name} ${name}.c ${PROJECT_BINARY_DIR}/config.h) target_link_libraries (${name} ${project_libraries}) add_threads (${name}) endforeach () -add_dependencies (kike replies) -add_dependencies (degesch replies) -target_link_libraries (degesch ${degesch_libraries}) +add_dependencies (xD replies) +add_dependencies (xC replies) +target_link_libraries (xC ${xC_libraries}) # Tests include (CTest) if (BUILD_TESTING) - add_executable (test-degesch $) - set_target_properties (test-degesch PROPERTIES COMPILE_DEFINITIONS TESTING) - target_link_libraries (test-degesch $) - add_threads (test-degesch) - add_dependencies (test-degesch replies) + add_executable (test-xC $) + set_target_properties (test-xC PROPERTIES COMPILE_DEFINITIONS TESTING) + target_link_libraries (test-xC $) + add_threads (test-xC) + add_dependencies (test-xC replies) - add_test (NAME test-degesch COMMAND test-degesch) + add_test (NAME test-xC COMMAND test-xC) add_test (NAME custom-static-analysis COMMAND ${PROJECT_SOURCE_DIR}/test-static) endif () @@ -182,13 +182,13 @@ add_custom_target (clang-tidy WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) # Installation -install (TARGETS zyklonb degesch kike DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) # XXX: our defaults for XDG_DATA_DIRS expect /usr/local/shore or /usr/share -install (DIRECTORY plugins/zyklonb/ - DESTINATION ${CMAKE_INSTALL_DATADIR}/zyklonb/plugins USE_SOURCE_PERMISSIONS) -install (DIRECTORY plugins/degesch/ - DESTINATION ${CMAKE_INSTALL_DATADIR}/degesch/plugins) +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) @@ -196,7 +196,7 @@ if (NOT ASCIIDOCTOR_EXECUTABLE) message (FATAL_ERROR "asciidoctor not found") endif () -foreach (page zyklonb degesch kike) +foreach (page xB xC xD) set (page_output "${PROJECT_BINARY_DIR}/${page}.1") list (APPEND project_MAN_PAGES "${page_output}") add_custom_command (OUTPUT ${page_output} @@ -217,7 +217,7 @@ foreach (page ${project_MAN_PAGES}) endforeach () # CPack -set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Unethical IRC client, daemon and bot") +set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Unreasonable IRC client, daemon and bot") set (CPACK_PACKAGE_VERSION "${project_version_safe}") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch ") diff --git a/NEWS b/NEWS index ce42717..87338e3 100644 --- a/NEWS +++ b/NEWS @@ -1,29 +1,31 @@ -1.3.0 (20xx-xx-xx) +1.3.0 (2021-xx-xx) - * degesch: made nick autocompletion offer recent speakers first + * xC: made nick autocompletion offer recent speakers first + + * All binaries have been renamed to something even sillier 1.2.0 (2021-07-08) "There Are Other Countries As Well" - * degesch: added a /squery command for IRCnet + * xC: added a /squery command for IRCnet - * degesch: added trivial support for SASL EXTERNAL, enabled by adding "sasl" + * xC: added trivial support for SASL EXTERNAL, enabled by adding "sasl" to the respective server's "capabilities" list - * degesch: now supporting IRCv3.2 capability negotiation, including CAP DEL + * xC: now supporting IRCv3.2 capability negotiation, including CAP DEL - * degesch: added support for IRCv3 chghost + * xC: added support for IRCv3 chghost - * degesch: /deop and /devoice without arguments will use the client's user + * xC: /deop and /devoice without arguments will use the client's user - * degesch: /set +=/-= now treats its argument as a string array + * xC: /set +=/-= now treats its argument as a string array - * degesch: made "/help /command" work the same way as "/help command" does + * xC: made "/help /command" work the same way as "/help command" does - * degesch: /ban and /unban don't mangle extended bans anymore + * xC: /ban and /unban don't mangle extended bans anymore - * degesch: joining new channels no longer switches to their buffer - automatically if the current input line isn't empty + * 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 @@ -31,9 +33,9 @@ 1.1.0 (2020-10-31) "What Do You Mean By 'This Isn't Germany'?" - * degesch: made fancy-prompt.lua work with libedit + * xC: made fancy-prompt.lua work with libedit - * kike: fixed a regression with an unspecified "bind_host" + * xD: fixed a regression with an unspecified "bind_host" * Miscellaneous minor improvements @@ -42,55 +44,55 @@ * Coming with real manual pages instead of help2man-generated stubs - * degesch: added support for more IRC colours and strike-through text (M-m x) + * xC: added support for more IRC colours and strike-through text (M-m x) - * degesch: now tolerating all UTF-8 messages cut off by the server + * xC: now tolerating all UTF-8 messages cut off by the server - * degesch: disabled "behaviour.backlog_helper_strip_formatting" by default + * xC: disabled "behaviour.backlog_helper_strip_formatting" by default since the relevant issue with ACS terminfo entries has been resolved - * degesch: enabled word wrapping in the backlog by default + * xC: enabled word wrapping in the backlog by default - * degesch: made the unread marker span the whole line, with a configurable + * xC: made the unread marker span the whole line, with a configurable character; the previous behaviour can be obtained by setting it empty - * degesch: fixed the prompt not showing back up after exiting a backlog helper + * xC: fixed the prompt not showing back up after exiting a backlog helper when an external event has provoked an attempt to change it - * degesch: now watching fellow channel users' away status when the server + * xC: now watching fellow channel users' away status when the server supports the away-notify capability; indicated by italicised nicknames - * degesch: added a plugin to highlight prime numbers in incoming messages + * xC: added a plugin to highlight prime numbers in incoming messages - * kike: make sure an unspecified "bind_host" binds to both IPv4 and IPv6 + * xD: make sure an unspecified "bind_host" binds to both IPv4 and IPv6 - * ZyklonB: install plugins to /usr/share and look for them in XDG data dirs + * 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" - * degesch: fixed a crash and prompt attribute output in libedit 20191231-3.1, + * xC: fixed a crash and prompt attribute output in libedit 20191231-3.1, though users are officially discouraged from using this library - * degesch: fixed Lua 5.4 build, so far the support is experimental + * xC: fixed Lua 5.4 build, so far the support is experimental * Miscellaneous little fixes 0.9.7 (2018-10-21) "Business as Usual" - * kike: fix wildcard handling in WHOIS + * xD: fix wildcard handling in WHOIS - * kike: properly handle STATS without parametetrs + * xD: properly handle STATS without parametetrs - * kike: abort earlier when an invalid mode character is detected while + * xD: abort earlier when an invalid mode character is detected while processing channel MODE messages - * kike: do not send NICK notifications when the nickname doesn't really change + * xD: do not send NICK notifications when the nickname doesn't really change - * kike: fix hostname string verification (only used for "server_name") + * xD: fix hostname string verification (only used for "server_name") 0.9.6 (2018-06-22) "I've Been Sitting Here All This Time" @@ -99,108 +101,108 @@ * Fix LibreSSL compatibility - * degesch: a second /disconnect cuts the connection by force + * xC: a second /disconnect cuts the connection by force - * degesch: send a QUIT message to the IRC server on Ctrl-C + * xC: send a QUIT message to the IRC server on Ctrl-C - * degesch: add a Slack plugin (even though the gateway's now defunct) + * xC: add a Slack plugin (even though the gateway's now defunct) - * degesch: show an error message on log write failure + * xC: show an error message on log write failure - * degesch: fix parsing of literal IPv6 addresses with port numbers + * xC: fix parsing of literal IPv6 addresses with port numbers - * degesch: fix some error messages + * xC: fix some error messages - * degesch: workaround a Readline bug in the fancy-prompt.lua plugin + * xC: workaround a Readline bug in the fancy-prompt.lua plugin - * kike: fix two memory leaks + * xD: fix two memory leaks - * kike: improve error handling for incoming connections + * xD: improve error handling for incoming connections - * kike: disable TLS session reuse + * xD: disable TLS session reuse 0.9.5 (2016-12-30) "It's Time" * Better support for the KILL command - * degesch: export many more fields to the Lua API, add a prompt hook + * xC: export many more fields to the Lua API, add a prompt hook - * degesch: show channel user count in the prompt + * xC: show channel user count in the prompt - * degesch: allow hiding join/part messages and other noise (Meta-Shift-H) + * xC: allow hiding join/part messages and other noise (Meta-Shift-H) - * degesch: allow autojoining channels with keys + * xC: allow autojoining channels with keys - * degesch: rejoin channels with keys on reconnect + * xC: rejoin channels with keys on reconnect - * degesch: make /query without arguments just open the buffer + * xC: make /query without arguments just open the buffer - * degesch: add a censor plugin + * xC: add a censor plugin - * degesch: die on configuration parse errors + * xC: die on configuration parse errors - * degesch: request channel modes also on rejoin + * xC: request channel modes also on rejoin - * degesch: don't show remembered channel modes on parted channels + * xC: don't show remembered channel modes on parted channels - * degesch: fix highlight detection in colored text + * xC: fix highlight detection in colored text - * degesch: fix CTCP handling for the real world and don't decode X-QUOTEs + * xC: fix CTCP handling for the real world and don't decode X-QUOTEs - * degesch: add support for OpenSSL 1.1.0 + * xC: add support for OpenSSL 1.1.0 0.9.4 (2016-04-28) "Oops" - * degesch: fix crash on characters invalid in Windows-1252 + * xC: fix crash on characters invalid in Windows-1252 - * degesch: add an auto-rejoin plugin + * xC: add an auto-rejoin plugin - * degesch: better date change messages with customizable formatting; + * xC: better date change messages with customizable formatting; now also used in the backlog, so it looks closer to regular output - * ZyklonB: add a calc plugin providing a basic Scheme REPL + * xB: add a calc plugin providing a basic Scheme REPL - * ZyklonB: add a seen plugin + * xB: add a seen plugin - * kike, ZyklonB: use pledge(2) on OpenBSD + * 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 - * degesch: now we erase the screen before displaying buffers + * xC: now we erase the screen before displaying buffers - * degesch: implemented word wrapping in buffers + * xC: implemented word wrapping in buffers - * degesch: added autocomplete for /topic + * xC: added autocomplete for /topic - * degesch: Lua API was improved and extended + * xC: Lua API was improved and extended - * degesch: added a basic last.fm "now playing" plugin + * xC: added a basic last.fm "now playing" plugin - * degesch: backlog limit was made configurable + * xC: backlog limit was made configurable - * degesch: allow changing the list of IRC capabilities to use if available + * xC: allow changing the list of IRC capabilities to use if available - * degesch: optimize buffer memory usage + * xC: optimize buffer memory usage - * degesch: added logging of messages sent from /quote and plugins + * xC: added logging of messages sent from /quote and plugins - * degesch: M-! and M-a to go to the next buffer in order with - a highlight or new activity respectively + * xC: M-! and M-a to go to the next buffer in order with a highlight + or new activity respectively - * degesch: added --format for previewing things like MOTD files + * xC: added --format for previewing things like MOTD files - * degesch: added /buffer goto supporting case insensitive partial matches + * xC: added /buffer goto supporting case insensitive partial matches - * kike: add support for IRCv3.2 server-time + * xD: add support for IRCv3.2 server-time - * ZyklonB: plugins now run in a dedicated data directory + * xB: plugins now run in a dedicated data directory - * ZyklonB: added a factoids plugin + * xB: added a factoids plugin * Remote addresses are now resolved asynchronously @@ -209,28 +211,28 @@ 0.9.2 (2015-12-31) - * degesch: added rudimentary support for Lua scripting + * xC: added rudimentary support for Lua scripting - * degesch: added detection of pasting, so that it doesn't trigger other + * xC: added detection of pasting, so that it doesn't trigger other keyboard shortcuts, such as for autocomplete - * degesch: added auto-away capability + * xC: added auto-away capability - * degesch: added an /oper command + * xC: added an /oper command - * degesch: libedit backend works again + * xC: libedit backend works again - * degesch: added capability to edit the input line using VISUAL/EDITOR + * xC: added capability to edit the input line using VISUAL/EDITOR - * degesch: added Meta-Tab to switch to the last used buffer + * xC: added Meta-Tab to switch to the last used buffer - * degesch: correctly respond to stopping and resuming (SIGTSTP) + * xC: correctly respond to stopping and resuming (SIGTSTP) - * degesch: fixed decoding of text formatting + * xC: fixed decoding of text formatting - * degesch: unseen PMs now show up as highlights + * xC: unseen PMs now show up as highlights - * degesch: various bugfixes + * xC: various bugfixes 0.9.1 (2015-09-25) @@ -241,23 +243,23 @@ * Pulled in kqueue support - * degesch: added backlog/scrollback functionality using less(1) + * xC: added backlog/scrollback functionality using less(1) - * degesch: made showing the entire set of channel mode user prefixes optional + * xC: made showing the entire set of channel mode user prefixes optional - * degesch: nicknames in /names are now ordered + * xC: nicknames in /names are now ordered - * degesch: nicknames now use the 256-color terminal palette if available + * xC: nicknames now use the 256-color terminal palette if available - * degesch: now we skip entries in the "addresses" list that can't be resolved + * xC: now we skip entries in the "addresses" list that can't be resolved to an address, along with displaying a more helpful message - * degesch: joins, parts, nick changes and quits don't count as new buffer + * xC: joins, parts, nick changes and quits don't count as new buffer activity anymore - * degesch: added Meta-H to open the full log file + * xC: added Meta-H to open the full log file - * degesch: various bugfixes and little improvements + * xC: various bugfixes and little improvements 0.9.0 (2015-07-23) diff --git a/README.adoc b/README.adoc index f4200e5..ef721aa 100644 --- a/README.adoc +++ b/README.adoc @@ -2,25 +2,25 @@ uirc3 ===== :compact-option: -The [line-through]#unethical# edgy IRC trinity. This project consists of an -IRC client, daemon, and bot. It's all you're ever going to need for chatting, -as long as you can make do with minimalist software. +The unreasonable IRC trinity. This project consists of an IRC client, daemon, +and bot. It's all you're ever going to need for chatting, as long as you can +make do with minimalist software. All of them have these potentially interesting properties: - IPv6 support - TLS support, including client certificates - - lean on dependencies (with the exception of 'degesch') + - lean on dependencies (with the exception of 'xC') - compact and arguably easy to hack on - very permissive license -degesch -------- +xC +-- The IRC client. It is largely defined by being built on top of GNU Readline that has been hacked to death. Its interface should feel somewhat familiar for weechat or irssi users. -image::degesch.png[align="center"] +image::xC.png[align="center"] This is the largest application within the project. It has most of the stuff you'd expect of an IRC client, such as being able to set up multiple servers, @@ -28,8 +28,8 @@ a powerful configuration system, integrated help, text formatting, CTCP queries, automatic splitting of overlong messages, autocomplete, logging to file, auto-away, command aliases and basic support for Lua scripting. -kike ----- +xD +-- The IRC daemon. It is designed to be used as a regular user application rather than a system-wide daemon. If all you want is a decent, minimal IRCd for testing purposes or a small network of respectful users (or bots), this one will @@ -48,7 +48,7 @@ Not supported: - server linking (which also means no services); I consider existing protocols for this purpose ugly and tricky to implement correctly; I've also found no use for this feature yet - - online changes to configuration; the configuration system from degesch could + - online changes to configuration; the configuration system from xC could be used to implement this feature if needed - limits of almost any kind, just connections and mode `+l` @@ -56,8 +56,8 @@ This program has been https://git.janouch.name/p/haven/src/branch/master/hid[ported to Go], and development continues over there. -ZyklonB -------- +xB +-- The IRC bot. It builds upon the concept of my other VitaminA IRC bot. The main characteristic of these two bots is that they run plugins as coprocesses, which allows for enhanced reliability and programming language freedom. @@ -78,8 +78,8 @@ Building -------- Build dependencies: CMake, pkg-config, asciidoctor, awk, liberty (included) + Runtime dependencies: openssl + -Additionally for degesch: curses, libffi, lua >= 5.3 (optional), - readline >= 6.0 or libedit >= 2013-07-12 +Additionally for xC: curses, libffi, lua >= 5.3 (optional), + readline >= 6.0 or libedit >= 2013-07-12 Avoid libedit if you can, in general it works but at the moment history is acting up and I have no clue about fixing it. @@ -102,49 +102,49 @@ Or you can try telling CMake to make a package for you. For Debian it is: Usage ----- -'degesch' has in-program configuration. Just run it and read the instructions. -Consult its link:degesch.adoc[man page] for details about the interface. +'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: - $ zyklonb --write-default-config - $ kike --write-default-config + $ 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: - $ zyklonb - $ kike + $ xB + $ xD -'ZyklonB' stays running in the foreground, therefore I recommend launching it -inside a Screen or tmux session. +'xB' stays running in the foreground, therefore I recommend launching it inside +a Screen or tmux session. -'kike', on the other hand, immediately forks into the background. Use the PID +'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. Client Certificates ------------------- -'degesch' 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. +'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. -'kike' uses SHA-1 fingerprints of TLS client certificates to authenticate users. +'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 degesch ------------------------------- -The default and preferred frontend used in 'degesch' is GNU Readline. This -means that you can change your bindings by editing '~/.inputrc'. For example: +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 degesch +$if xC "\e\e[C": move-buffer-right "\e\e[D": move-buffer-left $endif @@ -154,12 +154,12 @@ 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 degesch look like the screenshot? ------------------------------------------------ -First of all, you must build it with Lua support. With the defaults, degesch -doesn't look too fancy because I don't want to depend on Lua or 256-colour -terminals. In addition to that, I appear to be one of the few people who use -black on white terminals. +How do I make xC look like the screenshot? +------------------------------------------ +First of all, you must build it with Lua support. With the defaults, xC doesn't +look too fancy because I don't want to depend on Lua or 256-colour terminals. +In addition to that, I appear to be one of the few people who use black on white +terminals. /set behaviour.date_change_line = "%a %e %b %Y" /set behaviour.plugin_autoload += "fancy-prompt.lua" @@ -195,5 +195,5 @@ 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 'degesch' becomes GPL-licensed when you link it against GNU Readline +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/degesch.adoc b/degesch.adoc deleted file mode 100644 index f3c7904..0000000 --- a/degesch.adoc +++ /dev/null @@ -1,127 +0,0 @@ -degesch(1) -========== -:doctype: manpage -:manmanual: uirc3 Manual -:mansource: uirc3 {release-version} - -Name ----- -degesch - terminal-based IRC client - -Synopsis --------- -*degesch* [_OPTION_]... - -Description ------------ -*degesch* 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' | degesch -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 in the backlog helper, - which is almost certainly the *less*(1) program. -*M-h*: *display-full-log*:: - Show the log file for this buffer in the backlog helper. -*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, - *x* for struck-through, *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 ------ -*degesch* follows the XDG Base Directory Specification. - -_~/.config/degesch/degesch.conf_:: - The program's configuration file. Preferrably use internal facilities, such - as the */set* command, to make changes in it. - -_~/.local/share/degesch/logs/_:: - When enabled by *behaviour.logging*, log files are stored here. - -_~/.local/share/degesch/plugins/_:: -_/usr/local/share/degesch/plugins/_:: -_/usr/share/degesch/plugins/_:: - Plugins are searched for in these directories, in order. - -Bugs ----- -The editline (libedit) frontend is more of a proof of concept that mostly seems -to work but exhibits bugs that are not our fault. - -Reporting bugs --------------- -Use https://git.janouch.name/p/uirc3 to report bugs, request features, -or submit pull requests. - -See also --------- -*less*(1), *readline*(3) or *editline*(7) diff --git a/degesch.c b/degesch.c deleted file mode 100644 index 0f8f22b..0000000 --- a/degesch.c +++ /dev/null @@ -1,14473 +0,0 @@ -/* - * degesch.c: a terminal-based IRC client - * - * Copyright (c) 2015 - 2021, Přemysl Eric Janouch - * - * 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( RESET, reset, String to reset terminal attributes ) \ - 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 -{ -#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 "degesch" - -#include "common.c" -#include "kike-replies.c" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -// Literally cancer -#undef lines -#undef columns - -#include - -#ifdef HAVE_LUA -#include -#include -#include -#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 - char *(*get_line) (void *input); - /// 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 -#include - -#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) -{ - (void) input; - 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- 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 - -#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 int -input_el__get_termios (int character, int fallback) -{ - if (!g_terminal.initialized) - return fallback; - - cc_t value = g_terminal.termios.c_cc[character]; - if (value == _POSIX_VDISABLE) - return fallback; - return value; -} - -static void -input_el__redisplay (void *input) -{ - // See rl_redisplay() - struct input_el *self = input; - char x[] = { input_el__get_termios (VREPRINT, 'R' - 0x40), 0 }; - el_push (self->editline, x); - - // We have to do this or it gets stuck and nothing is done - int count = 0; - (void) el_wgets (self->editline, &count); -} - -static char * -input_el__make_prompt (EditLine *editline) -{ - struct input_el *self; - 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) -{ - struct input_el *self = input; - const LineInfo *info = el_line (self->editline); - 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); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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->current) - input_el__save_buffer (self, self->current); - - input_el__restore_buffer (self, buffer); - el_wset (self->editline, EL_HIST, history, buffer->history); - self->current = buffer; -} - -static void -input_el_buffer_destroy (void *input, input_buffer_t input_buffer) -{ - (void) input; - struct input_el_buffer *buffer = input_buffer; - - 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 editline */) - { - 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. - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// 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 }, - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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) - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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 mIRC 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 - int color; ///< Colour - 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 - - 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 }; - 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_STATUS = 1 << 0, ///< Status message - BUFFER_LINE_ERROR = 1 << 1, ///< Error message - BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this - BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file - BUFFER_LINE_INDENT = 1 << 4, ///< Just indent the line - BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam -}; - -struct buffer_line -{ - LIST_HEADER (struct buffer_line) - - int flags; ///< Flags - 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 - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// 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_mode; ///< Our current user modes - char *irc_user_host; ///< Our current user@host - bool autoaway_active; ///< Autoaway is currently active - - 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_mode), - 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_mode = str_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_mode); - free (self->irc_user_host); - - strv_free (&self->cap_ls_buf); - server_free_specifics (self); - free (self); -} - -REF_COUNTABLE_METHODS (server) -#define server_ref do_not_use_dangerous - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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); -}; - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct app_context -{ - char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes - - // Configuration: - - struct config config; ///< Program configuration - char *attrs[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 - - // Events: - - struct poller_fd tty_event; ///< Terminal input event - struct poller_fd signal_event; ///< Signal FD event - - 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_mirc_escape; ///< Awaiting a mIRC attribute escape - bool in_bracketed_paste; ///< User is pasting some content - struct str input_buffer; ///< Buffered pasted content - - bool running_backlog_helper; ///< Running a backlog helper - bool running_editor; ///< Running editor for the input - char *editor_filename; ///< The file being edited by user - int terminal_suspended; ///< Terminal suspension level - - 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->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_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); - for (size_t i = 0; i < ATTR_COUNT; i++) - { - free (self->attrs_defaults[i]); - free (self->attrs[i]); - } - - 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); - - 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_backlog_limit_change (struct config_item *item); -static void on_config_attribute_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_behaviour[] = -{ - { .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 = "beep_on_highlight", - .comment = "Beep when highlighted or on a new invisible PM", - .type = CONFIG_ITEM_BOOLEAN, - .default_ = "on", - .on_change = on_config_beep_on_highlight_change }, - { .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 }, - { .name = "date_change_line", - .comment = "Input to strftime(3) for the date change line", - .type = CONFIG_ITEM_STRING, - .default_ = "\"%F\"" }, - { .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 = "logging", - .comment = "Log buffer contents to file", - .type = CONFIG_ITEM_BOOLEAN, - .default_ = "off", - .on_change = on_config_logging_change }, - { .name = "save_on_quit", - .comment = "Save configuration before quitting", - .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 = "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 = "backlog_helper", - .comment = "Shell command to display a buffer's history", - .type = CONFIG_ITEM_STRING, - .default_ = "\"LESSSECURE=1 less -M -R +Gb\"" }, - { .name = "backlog_helper_strip_formatting", - .comment = "Strip formatting from backlog helper input", - .type = CONFIG_ITEM_BOOLEAN, - .default_ = "off" }, - - { .name = "reconnect_delay_growing", - .comment = "Growing factor for 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" }, - - { .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 = "plugin_autoload", - .comment = "Plugins to automatically load on start", - .type = CONFIG_ITEM_STRING_ARRAY, - .validate = config_validate_nonjunk_string }, - {} -}; - -static struct config_schema g_config_attributes[] = -{ -#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \ - .on_change = on_config_attribute_change }, - ATTR_TABLE (XX) -#undef XX - {} -}; - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -load_config_behaviour (struct config_item *subtree, void *user_data) -{ - config_schema_apply_to_object (g_config_behaviour, subtree, user_data); -} - -static void -load_config_attributes (struct config_item *subtree, void *user_data) -{ - config_schema_apply_to_object (g_config_attributes, 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, "behaviour", load_config_behaviour, ctx); - config_register_module (config, "attributes", load_config_attributes, 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); -} - -// --- 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; -} - -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; - - if (printer) - tputs (ctx->attrs[attribute], 1, printer); - - vfprintf (stream, fmt, ap); - - if (printer) - tputs (ctx->attrs[ATTR_RESET], 1, printer); -} - -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); -} - -static ssize_t -attr_by_name (const char *name) -{ - static const char *table[ATTR_COUNT] = - { -#define XX(x, y, z) [ATTR_ ## x] = #y, - ATTR_TABLE (XX) -#undef XX - }; - - for (size_t i = 0; i < N_ELEMENTS (table); i++) - if (!strcmp (name, table[i])) - return i; - return -1; -} - -static void -on_config_attribute_change (struct config_item *item) -{ - struct app_context *ctx = item->user_data; - ssize_t id = attr_by_name (item->schema->name); - if (id != -1) - { - cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL - ? ctx->attrs_defaults[id] - : item->value.string.str)); - } -} - -static void -init_colors (struct app_context *ctx) -{ - bool have_ti = init_terminal (); - char **defaults = ctx->attrs_defaults; - -#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "") - - INIT_ATTR (PROMPT, enter_bold_mode); - INIT_ATTR (RESET, exit_attribute_mode); - INIT_ATTR (DATE_CHANGE, enter_bold_mode); - INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]); - INIT_ATTR (WARNING, g_terminal.color_set_fg[COLOR_YELLOW]); - INIT_ATTR (ERROR, g_terminal.color_set_fg[COLOR_RED]); - - INIT_ATTR (EXTERNAL, g_terminal.color_set_fg[COLOR_WHITE]); - INIT_ATTR (TIMESTAMP, g_terminal.color_set_fg[COLOR_WHITE]); - INIT_ATTR (ACTION, g_terminal.color_set_fg[COLOR_RED]); - INIT_ATTR (USERHOST, g_terminal.color_set_fg[COLOR_CYAN]); - INIT_ATTR (JOIN, g_terminal.color_set_fg[COLOR_GREEN]); - INIT_ATTR (PART, g_terminal.color_set_fg[COLOR_RED]); - - char *highlight = have_ti ? xstrdup_printf ("%s%s%s", - g_terminal.color_set_fg[COLOR_YELLOW], - g_terminal.color_set_bg[COLOR_MAGENTA], - enter_bold_mode) : NULL; - INIT_ATTR (HIGHLIGHT, highlight); - free (highlight); - -#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; - - // Apply the default values so that we start with any formatting at all - config_schema_call_changed - (config_item_get (ctx->config.root, "attributes", NULL)); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// 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. - -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 -}; - -struct attr_printer -{ - char **attrs; ///< Named attributes - FILE *stream; ///< Output stream - bool dirty; ///< Attributes are set -}; - -#define ATTR_PRINTER_INIT(ctx, stream) { ctx->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 backlog helper--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, self->attrs[ATTR_RESET]); - - self->dirty = false; -} - -static void -attr_printer_apply_named (struct attr_printer *self, int attribute) -{ - attr_printer_reset (self); - if (attribute != ATTR_RESET) - { - attr_printer_tputs (self, self->attrs[attribute]); - self->dirty = true; - } -} - -// 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); - - 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; -} - -// --- 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 with unknown encoding -// #m inserts a mIRC-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_) -{ - 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_)) -#define FORMATTER_ADD_SIMPLE(self, attribute_) \ - FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = TEXT_ ## attribute_) - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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 char 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 * -formatter_parse_mirc_color (struct formatter *self, const char *s) -{ - if (!isdigit_ascii (*s)) - { - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = -1); - return s; - } - - int fg = *s++ - '0'; - if (isdigit_ascii (*s)) - fg = fg * 10 + (*s++ - '0'); - if (fg < 16) - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); - else - FORMATTER_ADD_ITEM (self, FG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[fg])); - - if (*s != ',' || !isdigit_ascii (s[1])) - return s; - s++; - - int bg = *s++ - '0'; - if (isdigit_ascii (*s)) - bg = bg * 10 + (*s++ - '0'); - if (bg < 16) - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); - else - FORMATTER_ADD_ITEM (self, BG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[bg])); - - return s; -} - -static void -formatter_parse_mirc (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); - } - - switch (c) - { - case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD); break; - case '\x11': /* monospace, N/A */ break; - case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC); break; - case '\x1e': FORMATTER_ADD_SIMPLE (self, CROSSED_OUT); break; - case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break; - case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE); break; - - case '\x03': - s = formatter_parse_mirc_color (self, s); - break; - case '\x0f': - FORMATTER_ADD_RESET (self); - break; - default: - 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_mirc (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; - 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; - size_t len, processed = 0; - while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps))) - { - hard_assert (len != (size_t) -2 && len != (size_t) -1); - processed += 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, 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_named (&state, attrs.named); - else - attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); - } - - 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, ¤t)) - { - // 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, "behaviour.date_change_line"); - if (!strftime (buf, sizeof buf, format, ¤t)) - { - 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) -{ - int flags = line->flags; - if (flags & BUFFER_LINE_INDENT) formatter_add (f, " "); - if (flags & BUFFER_LINE_STATUS) formatter_add (f, " - "); - if (flags & BUFFER_LINE_ERROR) formatter_add (f, "#a=!=#r ", ATTR_ERROR); - - 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, ¤t)) - print_error ("%s: %s", "localtime_r", strerror (errno)); - else if (!strftime (buf, sizeof buf, "%T", ¤t)) - 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, ¤t)) - print_error ("%s: %s", "gmtime_r", strerror (errno)); - else if (!strftime (buf, sizeof buf, "%F %T", ¤t)) - 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, int flags, struct formatter *f) -{ - if (!buffer) - buffer = ctx->global_buffer; - - struct buffer_line *line = buffer_line_new (f); - line->flags = flags; - // 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; - - 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, - int flags, 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, &f); - - va_end (ap); -} - -#define log_global(ctx, flags, ...) \ - log_full ((ctx), NULL, (ctx)->global_buffer, flags, __VA_ARGS__) -#define log_server(s, buffer, flags, ...) \ - log_full ((s)->ctx, s, (buffer), flags, __VA_ARGS__) - -#define log_global_status(ctx, ...) \ - log_global ((ctx), BUFFER_LINE_STATUS, __VA_ARGS__) -#define log_global_error(ctx, ...) \ - log_global ((ctx), BUFFER_LINE_ERROR, __VA_ARGS__) -#define log_global_indent(ctx, ...) \ - log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__) - -#define log_server_status(s, buffer, ...) \ - log_server ((s), (buffer), BUFFER_LINE_STATUS, __VA_ARGS__) -#define log_server_error(s, buffer, ...) \ - log_server ((s), (buffer), BUFFER_LINE_ERROR, __VA_ARGS__) - -#define log_global_debug(ctx, ...) \ - BLOCK_START \ - if (g_debug_mode) \ - log_global ((ctx), 0, "(*) " __VA_ARGS__); \ - BLOCK_END - -#define log_server_debug(s, ...) \ - BLOCK_START \ - if (g_debug_mode) \ - log_server ((s), (s)->buffer, 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_STATUS | BUFFER_LINE_UNIMPORTANT, \ - "You are now known as #n", (new_)) -#define log_nick(s, buffer, old, new_) \ - log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ - "#n is now known as #n", (old), (new_)) - -#define log_chghost_self(s, buffer, new_) \ - log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ - "You are now #N", (new_)) -#define log_chghost(s, buffer, old, new_) \ - log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ - "#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, "<#s#n> #m", (prefixes), (who), (text)) -#define log_outcoming_action(s, buffer, who, text) \ - log_server ((s), (buffer), 0, " #a*#r #n #m", ATTR_ACTION, (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_status ((s), (s)->buffer, "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, '/'); - 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 `#s': #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)); - - 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); - - 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, - "behaviour.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) -{ - // 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; - - // 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, 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; - - // And 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; - - log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE, - "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); - - 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 buffer *buffer) -{ - LIST_FOR_EACH (struct buffer_line, iter, buffer->lines) - buffer_line_destroy (iter); - - buffer->lines = buffer->lines_tail = NULL; - buffer->lines_count = 0; -} - -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); - - // XXX: this may be able to trigger the uniqueness assertion with non-UTF-8 - char *target_utf8 = irc_to_utf8 (target); - char *result = xstrdup_printf ("%s.%s", s->name, target_utf8); - free (target_utf8); - return result; -} - -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 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); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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, - "behaviour.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, - "behaviour.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_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; - - s->state = 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 - s->state = 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 - s->state = 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; - s->state = 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_mode); - cstr_set (&s->irc_user_host, NULL); - - 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_STATUS | BUFFER_LINE_UNIMPORTANT, - "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); - } - - refresh_prompt (s->ctx); -} - -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"); - s->state = 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); - - refresh_prompt (s->ctx); -} - -/// Unwrap IPv6 addresses in format_host_port_pair() format -static void -irc_split_host_port (char *s, char **host, char **port) -{ - *host = s; - *port = "6667"; - - char *right_bracket = strchr (s, ']'); - if (s[0] == '[' && right_bracket) - { - *right_bracket = '\0'; - *host = s + 1; - s = right_bracket + 1; - } - - char *colon = strchr (s, ':'); - if (colon) - { - *colon = '\0'; - *port = colon + 1; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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++) - { - char *host, *port; - irc_split_host_port (addresses->vector[i], &host, &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++) - { - char *host, *port; - irc_split_host_port (addresses->vector[i], &host, &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) - s->state = 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); - 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_mode.len) - str_append_printf (output, "(%s)", s->irc_user_mode.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 - && buffer->channel->users_len) - { - 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, ""); - - 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); - - if (have_attributes) - { - // XXX: to be completely correct, we should use tputs, but we cannot - input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s", - INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], - INPUT_END_IGNORE, - localized, - INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], - INPUT_END_IGNORE, - attributed_suffix)); - free (localized); - } - 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_mirc (&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); - return p.changes == p.usermode_changes; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static bool -mode_processor_apply_user (struct mode_processor *self) -{ - mode_processor_toggle (self, &self->s->irc_user_mode); - 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); -} - -// --- 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_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 - // TODO: also handle actions here - 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 }, - { "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 void -irc_handle_join (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; - - 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); - if (!*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, "#a-->#r #N #a#s#r #S", - ATTR_JOIN, 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, "#a<--#r #N #a#s#r #n", - ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target); - if (message) - formatter_add (&f, " (#m)", message); - log_formatter (s->ctx, buffer, 0, &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 (©, msg->params.vector + 1); - char *modes = strv_join (©, " "); - strv_free (©); - - 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, BUFFER_LINE_STATUS | flags, - "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 the PM buffer 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 = xstrdup_printf ("%s.%s", s->name, new_nickname); - buffer_rename (s->ctx, pm_buffer, x); - free (x); - } - - if (irc_is_this_us (s, msg->prefix)) - { - 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); - } - } - - // Finally rename the user as it should be safe now - 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)); -} - -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_STATUS | BUFFER_LINE_HIGHLIGHT, - "#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, "#a<--#r #N #a#s#r #S", - ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name); - if (message) - formatter_add (&f, " (#m)", message); - log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &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, 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, - " #a*#r #n #m", ATTR_HIGHLIGHT, msg->prefix, text->str); - else - log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, - "#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, "#a<--#r #N #a#s#r", - ATTR_PART, prefix, ATTR_PART, "has quit"); - if (reason) - formatter_add (&f, " (#m)", reason); - log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &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) - cstr_set (&channel->topic, xstrdup (topic)); - - if (buffer) - { - log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"", - msg->prefix, "has changed the topic to", topic); - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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 }, -}; - -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_utf8 - (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_mode); - cstr_set (&s->irc_user_host, NULL); - - s->state = IRC_REGISTERED; - refresh_prompt (s->ctx); - - // 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); - process_input_utf8 (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_mode); - 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) - cstr_set (&channel->topic, xstrdup (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_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 (©, msg->params.vector + !!msg->params.len); - - struct buffer *buffer = s->buffer; - int flags = BUFFER_LINE_STATUS; - 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_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 > 1) - { - struct buffer *x; - if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1]))) - buffer = x; - } - } - - if (buffer) - { - // Join the parameter vector back and send it to the server buffer - log_server (s, buffer, flags, "#&m", strv_join (©, " ")); - } - - strv_free (©); -} - -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 the most basic acceptable 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, size_t text_len, - size_t line_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 (line_len && word_len <= line_len) - { - if (word_len) - { - str_append_data (output, text, word_len); - - text += word_len; - eaten += word_len; - line_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 (const char *p = text; (size_t) (p - text) <= line_len; ) - { - eaten = p - text; - hard_assert (utf8_decode (&p, text_len - eaten) >= 0); - } - str_append_data (output, text, eaten); - return eaten; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static bool -wrap_message (const char *message, - int line_max, struct strv *output, struct error **e) -{ - if (line_max <= 0) - goto error; - - int message_left = strlen (message); - while (message_left > line_max) - { - struct str m = str_make (); - - size_t eaten = wrap_text_for_single_line - (message, message_left, line_max, &m); - if (!eaten) - { - str_free (&m); - goto error; - } - - strv_append_owned (output, str_steal (&m)); - message += eaten; - message_left -= eaten; - } - - if (message_left) - strv_append (output, message); - - return true; - -error: - // Well, that's just weird - error_set (e, - "Message splitting was unsuccessful as there was " - "too little room for UTF-8 characters"); - return false; -} - -/// 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) -{ - // :!@ - 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; - - // However we don't always have the full info for message splitting - if (!space_in_one_message) - strv_append (output, message); - else if (!wrap_message (message, space_in_one_message, output, e)) - return false; - return true; -} - -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); - int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1 - + strlen (prefix) + strlen (suffix); - - // We might also want to preserve attributes across splits but - // that would make this code a lot more complicated - - 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); - - 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)); - 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))) - { - // TODO: creation of buffer names should be centralized -> replace - // calls to buffer_rename() and manual setting of buffer names - // with something like buffer_autorename() -- just mind the mess - // in irc_handle_nick(), which can hopefully be simplified - char *x = NULL; - switch (buffer->type) - { - case BUFFER_PM: - x = xstrdup_printf ("%s.%s", s->name, buffer->user->nickname); - break; - case BUFFER_CHANNEL: - x = xstrdup_printf ("%s.%s", s->name, 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, - 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); - process_input_utf8 (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 int -lua_server_get_state (lua_State *L) -{ - struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info); - struct server *server = wrapper->object; - switch (server->state) - { - case IRC_DISCONNECTED: lua_pushstring (L, "disconnected"); break; - case IRC_CONNECTING: lua_pushstring (L, "connecting"); break; - case IRC_CONNECTED: lua_pushstring (L, "connected"); break; - case IRC_REGISTERED: lua_pushstring (L, "registered"); break; - case IRC_CLOSING: lua_pushstring (L, "closing"); break; - case IRC_HALF_CLOSED: lua_pushstring (L, "half_closed"); break; - } - 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_ctx_gc (lua_State *L) -{ - return lua_weak_gc (L, &lua_ctx_info); -} - -static luaL_Reg lua_plugin_library[] = -{ - // These are global functions: - - { "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: - - { "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 degesch 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); - 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, "behaviour.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 (a->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 void -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); - } - else - { - 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); - } -} - -static bool -handle_command_set_assign - (struct app_context *ctx, struct strv *all, char *arguments) -{ - 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; - } - for (size_t i = 0; i < all->len; i++) - { - char *key = cstr_cut_until (all->vector[i], " "); - handle_command_set_assign_item (ctx, key, new_, add, remove); - free (key); - } - config_item_destroy (new_); - 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 -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); - - // TODO: validate the name; maybe also while loading configuration - char *name = cut_word (&a->arguments); - if (!*a->arguments) - return false; - - 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 (¶ms, " %s", v->vector[i + k]); - } - - irc_send (s, "%s%s", modes.str, params.str); - - str_free (&modes); - str_free (¶ms); - } -} - -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", - "[ |